// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameServer.RemoteView; using System.Collections.Concurrent; using System.Runtime.InteropServices; using MUnique.OpenMU.DataModel; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.Network.PlugIns; using MUnique.OpenMU.PlugIns; /// /// Serializer for the appearance of a player, compatible with the client of version 0.95. /// [Guid("99616318-38BC-4C0A-A818-29D821B78DE2")] [PlugIn] [Display(Name = nameof(PlugInResources.AppearanceSerializer095_Name), Description = nameof(PlugInResources.AppearanceSerializer095_Description), ResourceType = typeof(PlugInResources))] [MinimumClient(0, 95, ClientLanguage.Invariant)] public class AppearanceSerializer095 : IAppearanceSerializer { /// /// A cache which holds the results of the serializer. /// private static readonly ConcurrentDictionary Cache = new(); private enum Pets { Angel = 0, Imp = 1, Unicorn = 2, Dinorant = 3, } private enum WingIndex { WingsOfElf = 0, WingsOfHeaven = 1, WingsOfSatan = 2, None = 3, } /// public int NeededSpace => 11; /// public void InvalidateCache(IAppearanceData appearance) { Cache.TryRemove(appearance, out _); appearance.AppearanceChanged -= this.OnAppearanceOfAppearanceChanged; } /// public void WriteAppearanceData(Span target, IAppearanceData appearance, bool useCache) { if (target.Length < this.NeededSpace) { throw new ArgumentException($"Target span too small. Actual size: {target.Length}; Required: {this.NeededSpace}.", nameof(target)); } if (useCache && Cache.TryGetValue(appearance, out var cached)) { cached.CopyTo(target); } else { this.WritePreviewCharSet(target, appearance); if (useCache) { var cacheEntry = target.Slice(0, this.NeededSpace).ToArray(); if (Cache.TryAdd(appearance, cacheEntry)) { appearance.AppearanceChanged += this.OnAppearanceOfAppearanceChanged; } } } } private void OnAppearanceOfAppearanceChanged(object? sender, EventArgs args) => this.InvalidateCache(sender as IAppearanceData ?? throw new ArgumentException($"sender must be of type {nameof(IAppearanceData)}")); /// /// Writes the preview character set. /// /// The target. /// The appearance data. private void WritePreviewCharSet(Span target, IAppearanceData appearanceData) { ItemAppearance?[] itemArray = new ItemAppearance[InventoryConstants.EquippableSlotsCount]; for (byte i = 0; i < itemArray.Length; i++) { itemArray[i] = appearanceData.EquippedItems.FirstOrDefault(item => item.ItemSlot == i && item.Definition?.Number < 16); } if (appearanceData.CharacterClass is not null) { target[0] = (byte)(appearanceData.CharacterClass.Number << 3); /* 00 Dark Wizard 10 Soul Master 20 Dark Knight 30 Blade Knight 40 Elf 50 Muse Elf 60 Magic Gladiator*/ } target[0] |= (byte)appearanceData.Pose; this.SetHand(target, itemArray[InventoryConstants.LeftHandSlot], 1); this.SetHand(target, itemArray[InventoryConstants.RightHandSlot], 2); this.SetArmorPiece(target, itemArray[InventoryConstants.HelmSlot], 3, true, 0x80); this.SetArmorPiece(target, itemArray[InventoryConstants.ArmorSlot], 3, false, 0x40); this.SetArmorPiece(target, itemArray[InventoryConstants.PantsSlot], 4, true, 0x20); this.SetArmorPiece(target, itemArray[InventoryConstants.GlovesSlot], 4, false, 0x10); this.SetArmorPiece(target, itemArray[InventoryConstants.BootsSlot], 5, true, 0x08); target[5] |= (byte)((itemArray[InventoryConstants.WingsSlot]?.Definition?.Number & 0x03) << 2 ?? 0b1100); this.SetPet(target, itemArray[InventoryConstants.PetSlot]); // index9: upper 5 bits are the equipped flags of SetArmorPiece this.SetItemLevels(target, itemArray); } private void SetPet(Span preview, ItemAppearance? item) { var pet = (Pets?)item?.Definition?.Number; switch (pet) { case Pets.Angel: case Pets.Imp: case Pets.Unicorn: preview[5] |= (byte)pet; break; case Pets.Dinorant: preview[5] |= 0b11; preview[9] |= 0b100; break; default: preview[5] |= 0b11; break; } } private void SetHand(Span preview, ItemAppearance? item, int index) { if (item?.Definition is null) { preview[index] = 0xFF; } else { preview[index] = (byte)item.Definition.Number; preview[index] |= (byte)(item.Definition.Group << 5); } } private byte GetOrMaskForHighNibble(int value) { return (byte)((value << 4) & 0xF0); } private byte GetOrMaskForLowNibble(int value) { return (byte)(value & 0x0F); } private void SetArmorPiece(Span preview, ItemAppearance? item, int firstIndex, bool firstIndexHigh, byte secondIndexMask) { if (item?.Definition is null) { this.SetEmptyArmor(preview, firstIndex, firstIndexHigh, secondIndexMask); } else { // item id this.SetArmorItemIndex(preview, item, firstIndex, firstIndexHigh, secondIndexMask); // exc bit if (this.IsExcellent(item)) { preview[10] |= secondIndexMask; } } } private void SetEmptyArmor(Span preview, int firstIndex, bool firstIndexHigh, byte secondIndexMask) { // if the item is not equipped every index bit is set to 1 preview[firstIndex] |= firstIndexHigh ? this.GetOrMaskForHighNibble(0x0F) : this.GetOrMaskForLowNibble(0x0F); preview[9] |= secondIndexMask; } private void SetArmorItemIndex(Span preview, ItemAppearance item, int firstIndex, bool firstIndexHigh, byte secondIndexMask) { preview[firstIndex] |= firstIndexHigh ? this.GetOrMaskForHighNibble(item.Definition!.Number) : this.GetOrMaskForLowNibble(item.Definition!.Number); byte multi = (byte)(item.Definition.Number / 16); if (multi > 0) { byte bit1 = (byte)(multi % 2); if (bit1 == 1) { preview[9] |= secondIndexMask; } } } private bool IsExcellent(ItemAppearance item) { return item.VisibleOptions.Contains(ItemOptionTypes.Excellent); } private void SetItemLevels(Span preview, ItemAppearance?[] itemArray) { int levelIndex = 0; for (int i = 0; i < 7; i++) { if (itemArray[i] is not null) { levelIndex |= itemArray[i]!.GetGlowLevel() << (i * 3); } } preview[6] = (byte)((levelIndex >> 16) & 255); preview[7] = (byte)((levelIndex >> 8) & 255); preview[8] = (byte)(levelIndex & 255); } private void AddWing(Span preview, ItemAppearance? wing) { if (wing?.Definition is null) { preview[5] |= 0x0C; return; } preview[5] |= (byte)((wing?.Definition?.Number & 0x03) << 2 ?? 0b1100); } }