From f2c14375cfc2ed55f692ec7649e524aa092b72f5 Mon Sep 17 00:00:00 2001 From: Tim Sarbin Date: Wed, 5 Dec 2018 22:29:45 -0500 Subject: [PATCH] Initial commit for animation decoding (not finished). Minor fixups. --- OpenDiablo2.Common/Enums/eAnimationFrame.cs | 17 + OpenDiablo2.Common/Enums/eArmorType.cs | 27 ++ OpenDiablo2.Common/Enums/eCompositType.cs | 55 +++ OpenDiablo2.Common/Enums/eDrawEffect.cs | 29 ++ OpenDiablo2.Common/Enums/eHero.cs | 17 + OpenDiablo2.Common/Enums/eMobMode.cs | 117 +++++ OpenDiablo2.Common/Enums/eWeaponClass.cs | 53 +++ .../Interfaces/Data/IResourceManager.cs | 8 +- .../Interfaces/MessageBus/ISessionManager.cs | 2 - OpenDiablo2.Common/Models/AnimationData.cs | 47 ++ OpenDiablo2.Common/Models/BitMuncher.cs | 60 +++ OpenDiablo2.Common/Models/ImageSet.cs | 15 +- OpenDiablo2.Common/Models/MPQCOF.cs | 74 +++ OpenDiablo2.Common/Models/MPQDCC.cs | 440 ++++++++++++++++++ OpenDiablo2.Common/OpenDiablo2.Common.csproj | 11 + OpenDiablo2.Common/ResourcePaths.cs | 5 + OpenDiablo2.Core/EngineDataManager.cs | 5 + OpenDiablo2.Core/GameEngine.cs | 3 +- OpenDiablo2.Core/ResourceManager.cs | 32 ++ OpenDiablo2.Scenes/MainMenu.cs | 4 + OpenDiablo2.Scenes/SelectHeroClass.cs | 2 - OpenDiablo2.ServiceBus/SessionManager.cs | 1 - 22 files changed, 1007 insertions(+), 17 deletions(-) create mode 100644 OpenDiablo2.Common/Enums/eAnimationFrame.cs create mode 100644 OpenDiablo2.Common/Enums/eArmorType.cs create mode 100644 OpenDiablo2.Common/Enums/eCompositType.cs create mode 100644 OpenDiablo2.Common/Enums/eDrawEffect.cs create mode 100644 OpenDiablo2.Common/Enums/eMobMode.cs create mode 100644 OpenDiablo2.Common/Enums/eWeaponClass.cs create mode 100644 OpenDiablo2.Common/Models/AnimationData.cs create mode 100644 OpenDiablo2.Common/Models/BitMuncher.cs create mode 100644 OpenDiablo2.Common/Models/MPQCOF.cs create mode 100644 OpenDiablo2.Common/Models/MPQDCC.cs diff --git a/OpenDiablo2.Common/Enums/eAnimationFrame.cs b/OpenDiablo2.Common/Enums/eAnimationFrame.cs new file mode 100644 index 00000000..37642483 --- /dev/null +++ b/OpenDiablo2.Common/Enums/eAnimationFrame.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Enums +{ + public enum eAnimationFrame + { + NoEvent, + Attack, + Missile, + Sound, + Skill + } +} diff --git a/OpenDiablo2.Common/Enums/eArmorType.cs b/OpenDiablo2.Common/Enums/eArmorType.cs new file mode 100644 index 00000000..b3ef95a7 --- /dev/null +++ b/OpenDiablo2.Common/Enums/eArmorType.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Enums +{ + public enum eArmorType + { + Lite, + Medium, + Heavy + } + + public static class eArmorTypeHelper + { + private static readonly Dictionary tokens = new Dictionary() + { + { eArmorType.Lite , "lit" }, + { eArmorType.Medium , "med" }, + { eArmorType.Heavy , "hvy" } + }; + public static string ToToken(this eArmorType armorType) => tokens[armorType]; + public static eArmorType ToArmorType(this string source) => tokens.First(x => x.Value.ToUpper() == source.ToUpper()).Key; + } +} diff --git a/OpenDiablo2.Common/Enums/eCompositType.cs b/OpenDiablo2.Common/Enums/eCompositType.cs new file mode 100644 index 00000000..d090212a --- /dev/null +++ b/OpenDiablo2.Common/Enums/eCompositType.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Enums +{ + public enum eCompositType + { + Head, + Torso, + Legs, + RightArm, + LeftArm, + RightHand, + LeftHand, + Shield, + Special1, + Special2, + Special3, + Special4, + Special5, + Special6, + Special7, + Special8 + } + + public static class eCompositeTypeHelper + { + private readonly static Dictionary tokens = new Dictionary + { + { eCompositType.Head , "HD" }, + { eCompositType.Torso , "TR" }, + { eCompositType.Legs , "LG" }, + { eCompositType.RightArm , "RA" }, + { eCompositType.LeftArm , "LA" }, + { eCompositType.RightHand , "RH" }, + { eCompositType.LeftHand , "LH" }, + { eCompositType.Shield , "SH" }, + { eCompositType.Special1 , "S1" }, + { eCompositType.Special2 , "S2" }, + { eCompositType.Special3 , "S3" }, + { eCompositType.Special4 , "S4" }, + { eCompositType.Special5 , "S5" }, + { eCompositType.Special6 , "S6" }, + { eCompositType.Special7 , "S7" }, + { eCompositType.Special8 , "S8" } + }; + + public static string ToToken(this eCompositType source) => tokens[source]; + public static eCompositType ToCompositeType(this string source) => tokens.First(x => x.Value.ToUpper() == source.ToUpper()).Key; + + } +} diff --git a/OpenDiablo2.Common/Enums/eDrawEffect.cs b/OpenDiablo2.Common/Enums/eDrawEffect.cs new file mode 100644 index 00000000..f16255ab --- /dev/null +++ b/OpenDiablo2.Common/Enums/eDrawEffect.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Enums +{ + public enum eDrawEffect + { + //75 % transparency (colormaps 561-816 in a .pl2) + PctTransparency75, + + //50 % transparency (colormaps 305-560 in a .pl2) + PctTransparency50, + + //25 % transparency (colormaps 49-304 in a .pl2) + PctTransparency25, + + //Screen (colormaps 817-1072 in a .pl2) + Screen, + + //luminance (colormaps 1073-1328 in a .pl2) + Luminance, + + //bright alpha blending (colormaps 1457-1712 in a .pl2) + BringAlphaBlending + } +} diff --git a/OpenDiablo2.Common/Enums/eHero.cs b/OpenDiablo2.Common/Enums/eHero.cs index 06782633..dfc3d0de 100644 --- a/OpenDiablo2.Common/Enums/eHero.cs +++ b/OpenDiablo2.Common/Enums/eHero.cs @@ -16,4 +16,21 @@ namespace OpenDiablo2.Common.Enums Amazon, Druid } + + public static class eHeroExtensions + { + public readonly static Dictionary tokens = new Dictionary + { + { eHero.Amazon , "AM" }, + { eHero.Sorceress , "SO" }, + { eHero.Necromancer , "NE" }, + { eHero.Paladin , "PA" }, + { eHero.Barbarian , "BA" }, + { eHero.Druid , "DZ" }, + { eHero.Assassin , "AI" } + }; + + public static string ToToken(this eHero source) => tokens[source]; + public static eHero ToHero(this string source) => tokens.First(x => x.Value.ToUpper() == source.ToUpper()).Key; + } } diff --git a/OpenDiablo2.Common/Enums/eMobMode.cs b/OpenDiablo2.Common/Enums/eMobMode.cs new file mode 100644 index 00000000..994bdd2d --- /dev/null +++ b/OpenDiablo2.Common/Enums/eMobMode.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Enums +{ + public enum eMobMode + { + PlayerDeath, + PlayerNeutral, + PlayerWalk, + PlayerRun, + PlayerGetHit, + PlayerTownNeutral, + PlayerTownWalk, + PlayerAttack1, + PlayerAttack2, + PlayerBlock, + PlayerCast, + PlayerThrow, + PlayerKick, + PlayerSkill1, + PlayerSkill2, + PlayerSkill3, + PlayerSkill4, + PlayerDead, + PlayerSequence, + PlayerKnockBack, + MonsterDeath, + MonsterNeutral, + MonsterWalk, + MonsterGetHit, + MonsterAttack1, + MonsterAttack2, + MonsterBlock, + MonsterCast, + MonsterSkill1, + MonsterSkill2, + MonsterSkill3, + MonsterSkill4, + MonsterDead, + MonsterKnockback, + MonsterSequence, + MonsterRun, + ObjectNeutral, + ObjectOperating, + ObjectOpened, + ObjectSpecial1, + ObjectSpecial2, + ObjectSpecial3, + ObjectSpecial4, + ObjectSpecial5 + + + } + + public static class eMobModeExtensions + { + private static readonly Dictionary mobModes = new Dictionary + { + { eMobMode.PlayerDeath ,"DT" }, + { eMobMode.PlayerNeutral ,"NU" }, + { eMobMode.PlayerWalk ,"WL" }, + { eMobMode.PlayerRun ,"RN" }, + { eMobMode.PlayerGetHit ,"GH" }, + { eMobMode.PlayerTownNeutral ,"TN" }, + { eMobMode.PlayerTownWalk ,"TW" }, + { eMobMode.PlayerAttack1 ,"A1" }, + { eMobMode.PlayerAttack2 ,"A2" }, + { eMobMode.PlayerBlock ,"BL" }, + { eMobMode.PlayerCast ,"SC" }, + { eMobMode.PlayerThrow ,"TH" }, + { eMobMode.PlayerKick ,"KK" }, + { eMobMode.PlayerSkill1 ,"S1" }, + { eMobMode.PlayerSkill2 ,"S2" }, + { eMobMode.PlayerSkill3 ,"S3" }, + { eMobMode.PlayerSkill4 ,"S4" }, + { eMobMode.PlayerDead ,"DD" }, + { eMobMode.PlayerSequence ,"GH" }, + { eMobMode.PlayerKnockBack ,"GH" }, + + { eMobMode.MonsterDeath , "DT" }, + { eMobMode.MonsterNeutral , "NU" }, + { eMobMode.MonsterWalk , "WL" }, + { eMobMode.MonsterGetHit , "GH" }, + { eMobMode.MonsterAttack1 , "A1" }, + { eMobMode.MonsterAttack2 , "A2" }, + { eMobMode.MonsterBlock , "BL" }, + { eMobMode.MonsterCast , "SC" }, + { eMobMode.MonsterSkill1 , "S1" }, + { eMobMode.MonsterSkill2 , "S2" }, + { eMobMode.MonsterSkill3 , "S3" }, + { eMobMode.MonsterSkill4 , "S4" }, + { eMobMode.MonsterDead , "DD" }, + { eMobMode.MonsterKnockback , "GH" }, + { eMobMode.MonsterSequence , "xx" }, + { eMobMode.MonsterRun , "RN" }, + + { eMobMode.ObjectNeutral , "NU" }, + { eMobMode.ObjectOperating , "OP" }, + { eMobMode.ObjectOpened , "ON" }, + { eMobMode.ObjectSpecial1 , "S1" }, + { eMobMode.ObjectSpecial2 , "S2" }, + { eMobMode.ObjectSpecial3 , "S3" }, + { eMobMode.ObjectSpecial4 , "S4" }, + { eMobMode.ObjectSpecial5 , "S5" } + + + }; + + public static string ToToken(this eMobMode src) => mobModes[src]; + public static eMobMode FromToken(this string token) => mobModes.First(x => x.Value.ToUpper() == token.ToUpper()).Key; + } + +} diff --git a/OpenDiablo2.Common/Enums/eWeaponClass.cs b/OpenDiablo2.Common/Enums/eWeaponClass.cs new file mode 100644 index 00000000..5d38ad74 --- /dev/null +++ b/OpenDiablo2.Common/Enums/eWeaponClass.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Enums +{ + public enum eWeaponClass + { + None, + HandToHand, + Bow, + OneHandSwing, + OneHandThrust, + Staff, + TwoHandSwing, + TwoHandThrust, + Crossbow, + LeftJabRightSwing, + LeftJabRightThrust, + LeftSwingRightSwing, + LeftSwingRightThrust, + OneHandToHand, + TwoHandToHand + } + + public static class eWeaponClassExtensions + { + private static readonly Dictionary codes = new Dictionary + { + {eWeaponClass.None , "" }, + {eWeaponClass.HandToHand , "hth" }, + {eWeaponClass.Bow , "bow" }, + {eWeaponClass.OneHandSwing , "1hs" }, + {eWeaponClass.OneHandThrust , "1ht" }, + {eWeaponClass.Staff , "stf" }, + {eWeaponClass.TwoHandSwing , "2hs" }, + {eWeaponClass.TwoHandThrust , "2ht" }, + {eWeaponClass.Crossbow , "xbw" }, + {eWeaponClass.LeftJabRightSwing , "1js" }, + {eWeaponClass.LeftJabRightThrust , "1jt" }, + {eWeaponClass.LeftSwingRightSwing , "1ss" }, + {eWeaponClass.LeftSwingRightThrust , "1st" }, + {eWeaponClass.OneHandToHand , "ht1" }, + {eWeaponClass.TwoHandToHand , "ht2" } + }; + + public static string ToToken(this eWeaponClass source) => codes[source]; + public static eWeaponClass ToWeaponClass(this string source) => codes.First(x => x.Value.ToUpper() == source.ToUpper()).Key; + + } +} diff --git a/OpenDiablo2.Common/Interfaces/Data/IResourceManager.cs b/OpenDiablo2.Common/Interfaces/Data/IResourceManager.cs index 2c771ed2..ce620d35 100644 --- a/OpenDiablo2.Common/Interfaces/Data/IResourceManager.cs +++ b/OpenDiablo2.Common/Interfaces/Data/IResourceManager.cs @@ -1,4 +1,6 @@ -using OpenDiablo2.Common.Models; +using System.Collections.Generic; +using OpenDiablo2.Common.Enums; +using OpenDiablo2.Common.Models; namespace OpenDiablo2.Common.Interfaces { @@ -17,5 +19,9 @@ namespace OpenDiablo2.Common.Interfaces MPQDS1 GetMPQDS1(string resourcePath, LevelPreset level, LevelDetail levelDetail, LevelType levelType); MPQDT1 GetMPQDT1(string resourcePath); Palette GetPalette(string paletteName); + MPQCOF GetPlayerAnimation(eHero hero, eWeaponClass weaponClass, eMobMode mobMode); + MPQDCC GetPlayerDCC(MPQCOF.COFLayer cofLayer, eArmorType armorType, Palette palette); + + Dictionary> Animations { get; } } } diff --git a/OpenDiablo2.Common/Interfaces/MessageBus/ISessionManager.cs b/OpenDiablo2.Common/Interfaces/MessageBus/ISessionManager.cs index 39641f16..fcf3b85e 100644 --- a/OpenDiablo2.Common/Interfaces/MessageBus/ISessionManager.cs +++ b/OpenDiablo2.Common/Interfaces/MessageBus/ISessionManager.cs @@ -10,8 +10,6 @@ namespace OpenDiablo2.Common.Interfaces public interface ISessionManager : ISessionEventProvider, IDisposable { - Guid PlayerId { get; } - void Initialize(); void Stop(); diff --git a/OpenDiablo2.Common/Models/AnimationData.cs b/OpenDiablo2.Common/Models/AnimationData.cs new file mode 100644 index 00000000..0df8ce40 --- /dev/null +++ b/OpenDiablo2.Common/Models/AnimationData.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Models +{ + public sealed class AnimationData + { + public string COFName { get; set; } + public int FramesPerDirection { get; set; } + public int AnimationSpeed { get; set; } + public byte[] Flags { get; set; } + + public static Dictionary> LoadFromStream(Stream stream) + { + var result = new Dictionary>(); + var br = new BinaryReader(stream); + + while(stream.Length > stream.Position) + { + var dataCount = br.ReadInt32(); + + for (int i = 0; i < dataCount; ++i) + { + var data = new AnimationData + { + COFName = Encoding.ASCII.GetString(br.ReadBytes(8)).Trim('\0'), + FramesPerDirection = br.ReadInt32(), + AnimationSpeed = br.ReadInt32(), + Flags = br.ReadBytes(144), + }; + + if (!result.ContainsKey(data.COFName.ToUpper())) + result[data.COFName.ToUpper()] = new List(); + + result[data.COFName.ToUpper()].Add(data); + } + } + + br.Dispose(); + return result; + } + } +} diff --git a/OpenDiablo2.Common/Models/BitMuncher.cs b/OpenDiablo2.Common/Models/BitMuncher.cs new file mode 100644 index 00000000..8fec01d9 --- /dev/null +++ b/OpenDiablo2.Common/Models/BitMuncher.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; + +namespace OpenDiablo2.Common.Models +{ + public sealed class BitMuncher + { + private readonly byte[] data; + + public int Offset { get; private set; } + public int BitsRead { get; private set; } + + public BitMuncher(byte[] data, int offset = 0) + { + this.data = data; + this.Offset = offset; + } + + public BitMuncher(BitMuncher source) + { + this.data = source.data; + this.Offset = source.Offset; + } + public int GetBit() + { + var result = (data[Offset / 8] >> (Offset % 8)) & 0x01; + Offset++; + BitsRead++; + return result; + } + + public void SkipBits(int bits) => Offset += bits; + + public byte GetByte() => (byte)GetBits(8); + public Int32 GetInt32() => MakeSigned(GetBits(32), 32); + + public int GetBits(int bits) + { + var result = 0; + for (var i = 0; i < bits; i++) + result |= (GetBit() << i); + + return result; + } + + public int GetSignedBits(int bits) => MakeSigned(GetBits(bits), bits); + + private int MakeSigned(int value, int bits) + { + var msbSet = ((value & (1 << (bits - 1))) != 0); + if (msbSet) + { + value = ~value + 1; + } + + return value; + } + + } +} diff --git a/OpenDiablo2.Common/Models/ImageSet.cs b/OpenDiablo2.Common/Models/ImageSet.cs index 6afaa765..ba4e91da 100644 --- a/OpenDiablo2.Common/Models/ImageSet.cs +++ b/OpenDiablo2.Common/Models/ImageSet.cs @@ -30,7 +30,7 @@ namespace OpenDiablo2.Common.Models { var i = x + (y * Width); if (i >= ImageData.Length) - return 0; + return 0; var index = ImageData[i]; if (index == -1) @@ -44,10 +44,6 @@ namespace OpenDiablo2.Common.Models { static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - private UInt32 version; - private UInt32 unknown1; // 01 00 00 00 ??? - private UInt32 unknown2; // 00 00 00 00 ??? - private UInt32 termination; // EE EE EE EE or CD CD CD CD ??? private UInt32[] framePointers; public ImageFrame[] Frames { get; private set; } @@ -57,12 +53,13 @@ namespace OpenDiablo2.Common.Models public static ImageSet LoadFromStream(Stream stream) { var br = new BinaryReader(stream); + var version = br.ReadUInt32(); + var unknown1 = br.ReadUInt32(); + var unknown2 = br.ReadUInt32(); + var termination = br.ReadUInt32(); + var result = new ImageSet { - version = br.ReadUInt32(), - unknown1 = br.ReadUInt32(), - unknown2 = br.ReadUInt32(), - termination = br.ReadUInt32(), Directions = br.ReadUInt32(), FramesPerDirection = br.ReadUInt32() }; diff --git a/OpenDiablo2.Common/Models/MPQCOF.cs b/OpenDiablo2.Common/Models/MPQCOF.cs new file mode 100644 index 00000000..5ff25880 --- /dev/null +++ b/OpenDiablo2.Common/Models/MPQCOF.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenDiablo2.Common.Enums; + +namespace OpenDiablo2.Common.Models +{ + public sealed class MPQCOF + { + public class COFLayer + { + public MPQCOF COF { get; internal set; } + public eCompositType CompositType { get; internal set; } + public byte Shadow { get; internal set; } + public bool IsTransparent { get; internal set; } + public eDrawEffect DrawEffect { get; internal set; } + public eWeaponClass WeaponClass { get; internal set; } + + public string GetDCCPath(eArmorType armorType) + => $"{ResourcePaths.PlayerAnimationBase}\\{COF.Hero.ToToken()}\\{CompositType.ToToken()}\\{COF.Hero.ToToken()}{CompositType.ToToken()}{armorType.ToToken()}{COF.MobMode.ToToken()}{COF.WeaponClass.ToToken()}.dcc"; + + } + + public eHero Hero { get; private set; } + public eWeaponClass WeaponClass { get; private set; } + public eMobMode MobMode { get; private set; } + public List Animations { get; private set; } + + public IEnumerable Layers { get; private set; } + public IEnumerable AnimationFrames { get; private set; } + + public static MPQCOF Load(Stream stream, Dictionary> animations, eHero hero, eWeaponClass weaponClass, eMobMode mobMode) + { + var result = new MPQCOF + { + WeaponClass = weaponClass, + MobMode = mobMode, + Hero = hero + }; + + var br = new BinaryReader(stream); + + var numLayers = br.ReadByte(); + var framesPerDir = br.ReadByte(); + var numDirections = br.ReadByte(); + + br.ReadBytes(25); // Skip 25 unknown bytes... + + var layers = new List(); + for (var layerIdx = 0; layerIdx < numLayers; layerIdx++) + { + var layer = new COFLayer(); + layer.COF = result; + layer.CompositType = (eCompositType)br.ReadByte(); + layer.Shadow = br.ReadByte(); + br.ReadByte(); // Unknown + layer.IsTransparent = br.ReadByte() != 0; + layer.DrawEffect = (eDrawEffect)br.ReadByte(); + layers.Add(layer); + layer.WeaponClass = Encoding.ASCII.GetString(br.ReadBytes(4)).Trim('\0').ToWeaponClass(); + } + result.Layers = layers; + result.AnimationFrames = br.ReadBytes(framesPerDir).Select(x => (eAnimationFrame)x); + + var cofName = $"{hero.ToToken()}{mobMode.ToToken()}{weaponClass.ToToken()}".ToUpper(); + result.Animations = animations[cofName]; + br.Dispose(); + return result; + } + } +} diff --git a/OpenDiablo2.Common/Models/MPQDCC.cs b/OpenDiablo2.Common/Models/MPQDCC.cs new file mode 100644 index 00000000..716f5fae --- /dev/null +++ b/OpenDiablo2.Common/Models/MPQDCC.cs @@ -0,0 +1,440 @@ +using System; +using System.Linq; +using System.IO; +using System.Collections.Generic; +using System.Drawing; + +namespace OpenDiablo2.Common.Models +{ + public sealed class MPQDCC + { + public struct PixelBufferEntry + { + public byte[] Value; + public int Frame; + public int FrameCellIndex; + } + + public struct Cell + { + public int Width; + public int Height; + public int XOffset; + public int YOffset; + } + + public sealed class MPQDCCDirectionFrame + { + public int Width { get; private set; } + public int Height { get; private set; } + public int XOffset { get; private set; } + public int YOffset { get; private set; } + public int NumberOfOptionalBytes { get; private set; } + public int NumberOfCodedBytes { get; private set; } + public bool FrameIsBottomUp { get; private set; } + public Rectangle Box { get; private set; } + public Cell[] Cells { get; private set; } + public int HorizontalCellCount { get; private set; } + public int VerticalCellCount { get; private set; } + + + + public MPQDCCDirectionFrame(BitMuncher bits, MPQDCCDirection direction) + { + var variable0 = bits.GetBits(direction.Variable0Bits); + Width = bits.GetBits(direction.WidthBits); + Height = bits.GetBits(direction.HeightBits); + XOffset = bits.GetSignedBits(direction.XOffsetBits); + YOffset = bits.GetSignedBits(direction.YOffsetBits); + NumberOfOptionalBytes = bits.GetBits(direction.OptionalDataBits); + NumberOfCodedBytes = bits.GetBits(direction.CodedBytesBits); + FrameIsBottomUp = bits.GetBit() == 1; + + Box = new Rectangle( + XOffset, + YOffset - Height + 1, + Width, + Height + ); + + } + + public void MakeCells(MPQDCCDirection direction) + { + var w = 4 - ((Box.Left - direction.Box.Left) % 4); // Width of the first column (in pixels) + if ((Box.Width - w) <= 1) + HorizontalCellCount = 1; + else + { + HorizontalCellCount = 2 + ((Box.Width - w - 1) / 4); + if (((Box.Width - w - 1) % 4) == 0) + HorizontalCellCount--; + } + + + var h = 4 - ((Box.Top - direction.Box.Top) % 4); // Height of the first column (in pixels) + if ((Box.Height - h) <= 1) + VerticalCellCount = 1; + else + { + VerticalCellCount = 2 + ((Box.Height - h - 1) / 4); + if (((Box.Height - h - 1) % 4) == 0) + VerticalCellCount--; + } + + Cells = new Cell[HorizontalCellCount * VerticalCellCount]; + + // Calculate the cell widths and heights + var cellWidths = new int[HorizontalCellCount]; + if (HorizontalCellCount == 1) + cellWidths[0] = Box.Width; + else + { + cellWidths[0] = w; + for (var i = 1; i < (HorizontalCellCount - 1); i++) + cellWidths[i] = 4; + cellWidths[HorizontalCellCount - 1] = Box.Width - w - (4 * (HorizontalCellCount - 2)); + } + + var cellHeights = new int[VerticalCellCount]; + if (VerticalCellCount == 1) + cellHeights[0] = Box.Height; + else + { + cellHeights[0] = h; + for (var i = 1; i < (VerticalCellCount - 1); i++) + cellHeights[i] = 4; + cellHeights[VerticalCellCount - 1] = Box.Height - h - (4 * (VerticalCellCount - 2)); + } + + Cells = new Cell[HorizontalCellCount * VerticalCellCount]; + var offsetY = Box.Top - direction.Box.Top; + for (var y = 0; y < VerticalCellCount; y++) + { + var offsetX = Box.Left - direction.Box.Left; + for (var x = 0; x < HorizontalCellCount; x++) + { + Cells[x + (y * HorizontalCellCount)] = new Cell + { + XOffset = offsetX, + YOffset = offsetY, + Width = cellWidths[x], + Height = cellHeights[y] + }; + offsetX += cellWidths[x]; + } + offsetY += cellHeights[y]; + } + } + } + public sealed class MPQDCCDirection + { + public int OutSizeCoded { get; private set; } + public int CompressionFlags { get; private set; } + public int Variable0Bits { get; private set; } + public int WidthBits { get; private set; } + public int HeightBits { get; private set; } + public int XOffsetBits { get; private set; } + public int YOffsetBits { get; private set; } + public int OptionalDataBits { get; private set; } + public int CodedBytesBits { get; private set; } + public int EqualCellsBitstreamSize { get; private set; } + public int PixelMaskBitstreamSize { get; private set; } + public int EncodingTypeBitsreamSize { get; private set; } + public int RawPixelCodesBitstreamSize { get; private set; } + public MPQDCCDirectionFrame[] Frames { get; private set; } + public int[] PaletteEntries { get; private set; } + public Rectangle Box { get; private set; } + public Cell[] Cells { get; private set; } + public int HorizontalCellCount { get; private set; } + public int VerticalCellCount { get; private set; } + public List PixelBuffer { get; private set; } + + private static readonly byte[] crazyBitTable = { 0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 26, 28, 30, 32 }; + public MPQDCCDirection(BitMuncher bm, MPQDCC file) + { + OutSizeCoded = bm.GetInt32(); + CompressionFlags = bm.GetBits(2); + Variable0Bits = crazyBitTable[bm.GetBits(4)]; + WidthBits = crazyBitTable[bm.GetBits(4)]; + HeightBits = crazyBitTable[bm.GetBits(4)]; + XOffsetBits = crazyBitTable[bm.GetBits(4)]; + YOffsetBits = crazyBitTable[bm.GetBits(4)]; + OptionalDataBits = crazyBitTable[bm.GetBits(4)]; + CodedBytesBits = crazyBitTable[bm.GetBits(4)]; + + Frames = new MPQDCCDirectionFrame[file.NumberOfFrames]; + + if (Variable0Bits != 0) + throw new ApplicationException("Variable0Bits is not 0. This is not ok."); + + // Load the frame headers + for (var frameIdx = 0; frameIdx < file.NumberOfFrames; frameIdx++) + Frames[frameIdx] = new MPQDCCDirectionFrame(bm, this); + + Box = new Rectangle + { + X = Frames.Min(z => z.Box.X), + Y = Frames.Min(z => z.Box.Y), + Width = Frames.Max(z => z.Box.Right - z.Box.Left), + Height = Frames.Max(z => z.Box.Bottom - z.Box.Top) + }; + + if (OptionalDataBits > 0) + throw new ApplicationException("Optional bits in DCC data is not currently supported."); + + if ((CompressionFlags & 0x2) > 0) + EqualCellsBitstreamSize = bm.GetBits(20); + + PixelMaskBitstreamSize = bm.GetBits(20); + + if ((CompressionFlags & 0x1) > 0) + { + EncodingTypeBitsreamSize = bm.GetBits(20); + RawPixelCodesBitstreamSize = bm.GetBits(20); + } + + + // PixelValuesKey + var paletteEntries = new List(); + for (var i = 0; i < 256; i++) + paletteEntries.Add(bm.GetBit() != 0); + + PaletteEntries = new int[paletteEntries.Count(x => x == true)]; + var paletteOffset = 0; + for (var i = 0; i < 256; i++) + { + if (!paletteEntries[i]) + continue; + + PaletteEntries[paletteOffset++] = i; + } + + + + var equalCellsBitstream = new BitMuncher(bm); + bm.SkipBits(EqualCellsBitstreamSize); + + var pixelMaskBitstream = new BitMuncher(bm); + bm.SkipBits(PixelMaskBitstreamSize); + + var encodingTypeBitsream = new BitMuncher(bm); + bm.SkipBits(EncodingTypeBitsreamSize); + + var rawPixelCodesBitstream = new BitMuncher(bm); + bm.SkipBits(RawPixelCodesBitstreamSize); + + var pixelCodeandDisplacement = new BitMuncher(bm); + + CalculateCellOffsets(); + + foreach (var frame in Frames) + frame.MakeCells(this); + + FillPixelBuffer(pixelCodeandDisplacement, equalCellsBitstream, pixelMaskBitstream, encodingTypeBitsream, rawPixelCodesBitstream); + + if (equalCellsBitstream.BitsRead != EqualCellsBitstreamSize) + throw new ApplicationException("Did not read the correct number of bits!"); + + if (pixelMaskBitstream.BitsRead != PixelMaskBitstreamSize) + throw new ApplicationException("Did not read the correct number of bits!"); + + if (encodingTypeBitsream.BitsRead != EncodingTypeBitsreamSize) + throw new ApplicationException("Did not read the correct number of bits!"); + + if (rawPixelCodesBitstream.BitsRead != RawPixelCodesBitstreamSize) + throw new ApplicationException("Did not read the correct number of bits!"); + + + bm.SkipBits(pixelCodeandDisplacement.BitsRead); + } + + private static readonly int[] pixelMaskLookup = new int[] { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 }; + private void FillPixelBuffer(BitMuncher pcd, BitMuncher ec, BitMuncher pm, BitMuncher et, BitMuncher rp) + { + UInt32 lastPixel = 0; + var cellBuffer = new int[HorizontalCellCount * VerticalCellCount]; // Store the offset to the cell buffer + for (var i = 0; i < cellBuffer.Length; i++) + cellBuffer[i] = -1; + + PixelBuffer = new List(); + + var frameIndex = -1; + UInt32 pixelMask = 0x00; + foreach (var frame in Frames) + { + frameIndex++; + var originCellX = (frame.Box.Left - Box.Left) / 4; + var originCellY = (frame.Box.Top - Box.Top) / 4; + + for (var cellY = 0; cellY < frame.VerticalCellCount; cellY++) + { + var currentCellY = cellY + originCellY; + for (var cellX = 0; cellX < frame.HorizontalCellCount; cellX++) + { + var currentCell = (originCellX + cellX) + (currentCellY * HorizontalCellCount); + var nextCell = false; + if (cellBuffer[currentCell] != -1) + { + var tmp = 0; + if (EqualCellsBitstreamSize > 0) + tmp = ec.GetBit(); + + if (tmp == 0) + pixelMask = (UInt32)pm.GetBits(4); + else + nextCell = true; + } + else pixelMask = 0x0F; + + + if (nextCell == false) + { + // Decode the pixels + UInt32[] pixelStack = new UInt32[4]; + int numberOfPixelBits = pixelMaskLookup[pixelMask]; + var encodingType = ((numberOfPixelBits != 0) && (EncodingTypeBitsreamSize > 0)) + ? et.GetBit() + : 0; + int decodedPixel = 0; + for (int i = 0; i < numberOfPixelBits; i++) + { + if (encodingType != 0) + { + pixelStack[i] = (UInt32)rp.GetBits(8); + } + else + { + pixelStack[i] = lastPixel; + var pixelDisplacement = pcd.GetBits(4); + pixelStack[i] += (UInt32)pixelDisplacement; + while (pixelDisplacement == 15) + { + pixelDisplacement = pcd.GetBits(4); + pixelStack[i] += (UInt32)pixelDisplacement; + } + } + if (pixelStack[i] == lastPixel) + { + pixelStack[i] = 0; + i = numberOfPixelBits; // Just break here.... + } + else + { + lastPixel = pixelStack[i]; + decodedPixel++; + } + } + + var oldEntry = cellBuffer[currentCell]; + var curIdx = decodedPixel - 1; + var newEntry = new PixelBufferEntry { Value = new byte[4] }; + for (int i = 0; i < 4; i++) + { + if ((pixelMask * (1 << i)) != 0) + { + if (curIdx >= 0) + newEntry.Value[i] = (byte)pixelStack[curIdx--]; + else + newEntry.Value[i] = 0; + } + else + newEntry.Value[i] = PixelBuffer[oldEntry].Value[i]; + } + newEntry.Frame = frameIndex; + newEntry.FrameCellIndex = XOffsetBits + (cellY * HorizontalCellCount); + PixelBuffer.Add(newEntry); + cellBuffer[currentCell] = PixelBuffer.Count() - 1; + } + } + } + } + } + + private void CalculateCellOffsets() + { + // Calculate the number of vertical and horizontal cells we need + HorizontalCellCount = 1 + (Box.Width - 1) / 4; + VerticalCellCount = 1 + (Box.Height - 1) / 4; + + // Calculate the cell widths + var cellWidths = new int[HorizontalCellCount]; + if (HorizontalCellCount == 1) + cellWidths[0] = Box.Width; + else + { + for (var i = 0; i < HorizontalCellCount - 1; i++) + cellWidths[i] = 4; + cellWidths[HorizontalCellCount - 1] = Box.Width - (4 * (HorizontalCellCount - 1)); + } + + // Calculate the cell heights + var cellHeights = new int[VerticalCellCount]; + if (VerticalCellCount == 1) + cellHeights[0] = Box.Height; + else + { + for (var i = 0; i < VerticalCellCount - 1; i++) + cellHeights[i] = 4; + cellHeights[VerticalCellCount - 1] = Box.Height - (4 * (VerticalCellCount - 1)); + } + + // Set the cell widths and heights in the cell buffer + Cells = new Cell[VerticalCellCount * HorizontalCellCount]; + var yOffset = 0; + for (var y = 0; y < VerticalCellCount; y++) + { + var xOffset = 0; + for (var x = 0; x < HorizontalCellCount; x++) + { + Cells[x + (y * HorizontalCellCount)] = new Cell + { + Width = cellWidths[x], + Height = cellHeights[y], + XOffset = xOffset, + YOffset = yOffset + }; + xOffset += 4; + } + yOffset += 4; + } + } + } + + + + public int Signature { get; private set; } + public int Version { get; private set; } + public int NumberOfDirections { get; private set; } + public int NumberOfFrames { get; private set; } + public MPQDCCDirection[] Directions { get; private set; } + + public MPQDCC(byte[] data, Palette palette) + { + var bm = new BitMuncher(data); + Signature = bm.GetByte(); + if (Signature != 0x74) + throw new ApplicationException("Signature expected to be 0x74 but it is not."); + + Version = bm.GetByte(); + NumberOfDirections = bm.GetByte(); + NumberOfFrames = bm.GetInt32(); + + if (bm.GetInt32() != 1) + throw new ApplicationException("This value isn't 1. It has to be 1."); + + var totalSizeCoded = bm.GetInt32(); + var directionOffsets = new int[NumberOfDirections]; + for (var i = 0; i < NumberOfDirections; i++) + directionOffsets[i] = bm.GetInt32(); + + Directions = new MPQDCCDirection[NumberOfDirections]; + for (var i = 0; i < NumberOfDirections; i++) + { + Directions[i] = new MPQDCCDirection(new BitMuncher(data, directionOffsets[i] * 8), this); + } + + } + } + +} diff --git a/OpenDiablo2.Common/OpenDiablo2.Common.csproj b/OpenDiablo2.Common/OpenDiablo2.Common.csproj index 334ddb8f..d224b892 100644 --- a/OpenDiablo2.Common/OpenDiablo2.Common.csproj +++ b/OpenDiablo2.Common/OpenDiablo2.Common.csproj @@ -21,6 +21,7 @@ x64 prompt MinimumRecommendedRules.ruleset + IDE0049 bin\x64\Release\ @@ -55,7 +56,12 @@ + + + + + @@ -66,6 +72,7 @@ + @@ -76,6 +83,10 @@ + + + + diff --git a/OpenDiablo2.Common/ResourcePaths.cs b/OpenDiablo2.Common/ResourcePaths.cs index 33e40c5f..0e7a539f 100644 --- a/OpenDiablo2.Common/ResourcePaths.cs +++ b/OpenDiablo2.Common/ResourcePaths.cs @@ -125,6 +125,11 @@ namespace OpenDiablo2.Common public static string LevelType = "data\\global\\excel\\LvlTypes.txt"; public static string LevelDetails = "data\\global\\excel\\Levels.txt"; + // --- Animations --- + public static string ObjectData = "data\\global\\objects"; + public static string AnimationData = "data\\global\\animdata.d2"; + public static string PlayerAnimationBase = "data\\global\\CHARS"; + } } diff --git a/OpenDiablo2.Core/EngineDataManager.cs b/OpenDiablo2.Core/EngineDataManager.cs index c6bf1901..b7f40d7f 100644 --- a/OpenDiablo2.Core/EngineDataManager.cs +++ b/OpenDiablo2.Core/EngineDataManager.cs @@ -11,6 +11,8 @@ namespace OpenDiablo2.Core { public sealed class EngineDataManager : IEngineDataManager { + static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + private readonly IMPQProvider mpqProvider; public List LevelPresets { get; internal set; } @@ -28,6 +30,7 @@ namespace OpenDiablo2.Core private void LoadLevelTypes() { + log.Info("Loading level types"); var data = mpqProvider .GetTextFile(ResourcePaths.LevelType) .Skip(1) @@ -42,6 +45,7 @@ namespace OpenDiablo2.Core private void LoadLevelPresets() { + log.Info("Loading level presets"); var data = mpqProvider .GetTextFile(ResourcePaths.LevelPreset) .Skip(1) @@ -56,6 +60,7 @@ namespace OpenDiablo2.Core private void LoadLevelDetails() { + log.Info("Loading level details"); var data = mpqProvider .GetTextFile(ResourcePaths.LevelDetails) .Skip(1) diff --git a/OpenDiablo2.Core/GameEngine.cs b/OpenDiablo2.Core/GameEngine.cs index 78da0e95..e8034c8a 100644 --- a/OpenDiablo2.Core/GameEngine.cs +++ b/OpenDiablo2.Core/GameEngine.cs @@ -28,8 +28,7 @@ namespace OpenDiablo2.Core private Dictionary soundTable = new Dictionary(); public Dictionary PaletteTable { get; private set; } = new Dictionary(); - - + public GameEngine( GlobalConfiguration globalConfig, IMPQProvider mpqProvider, diff --git a/OpenDiablo2.Core/ResourceManager.cs b/OpenDiablo2.Core/ResourceManager.cs index 141f6052..5a65d733 100644 --- a/OpenDiablo2.Core/ResourceManager.cs +++ b/OpenDiablo2.Core/ResourceManager.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using OpenDiablo2.Common; +using OpenDiablo2.Common.Enums; using OpenDiablo2.Common.Interfaces; using OpenDiablo2.Common.Models; @@ -17,11 +19,16 @@ namespace OpenDiablo2.Core private Dictionary MPQFonts = new Dictionary(); private Dictionary Palettes = new Dictionary(); private Dictionary DTs = new Dictionary(); + private Dictionary PlayerCOFs = new Dictionary(); + + public Dictionary> Animations { get; private set; } = new Dictionary>(); public ResourceManager(IMPQProvider mpqProvider, IEngineDataManager engineDataManager) { this.mpqProvider = mpqProvider; this.engineDataManager = engineDataManager; + + Animations = AnimationData.LoadFromStream(mpqProvider.GetStream(ResourcePaths.AnimationData)); } public ImageSet GetImageSet(string resourcePath) @@ -70,5 +77,30 @@ namespace OpenDiablo2.Core return DTs[resourcePath]; } + + public MPQCOF GetPlayerAnimation(eHero hero, eWeaponClass weaponClass, eMobMode mobMode) + { + var key = $"{hero.ToToken()}{mobMode.ToToken()}{weaponClass.ToToken()}"; + if (PlayerCOFs.ContainsKey(key)) + return PlayerCOFs[key]; + + var path = $"{ResourcePaths.PlayerAnimationBase}\\{hero.ToToken()}\\COF\\{hero.ToToken()}{mobMode.ToToken()}{weaponClass.ToToken()}.cof"; + var result = MPQCOF.Load(mpqProvider.GetStream(path), Animations, hero, weaponClass, mobMode); + PlayerCOFs[key] = result; + + return result; + } + + public MPQDCC GetPlayerDCC(MPQCOF.COFLayer cofLayer, eArmorType armorType, Palette palette) + { + byte[] binaryData; + using (var stream = mpqProvider.GetStream(cofLayer.GetDCCPath(armorType))) + { + binaryData = new byte[stream.Length]; + stream.Read(binaryData, 0, (int)stream.Length); + } + var result = new MPQDCC(binaryData, palette); + return result; + } } } diff --git a/OpenDiablo2.Scenes/MainMenu.cs b/OpenDiablo2.Scenes/MainMenu.cs index c6115690..1648317b 100644 --- a/OpenDiablo2.Scenes/MainMenu.cs +++ b/OpenDiablo2.Scenes/MainMenu.cs @@ -79,6 +79,10 @@ namespace OpenDiablo2.Scenes var loadingSprite = renderWindow.LoadSprite(ResourcePaths.LoadingScreen, Palettes.Loading, new Point(300, 400)); + // TODO: This is just a test + //var animation = resourceManager.GetPlayerAnimation(eHero.Necromancer, eWeaponClass.HandToHand, eMobMode.PlayerTownNeutral); + //var path = animation.Layers.First().GetDCCPath(eArmorType.Lite); + //var test = resourceManager.GetPlayerDCC(animation.Layers.First(), eArmorType.Lite, paletteProvider.PaletteTable["Units"]); // Pre-load all the scenes for now until we fix the sdl load problem var scenesToLoad = new string[] {"Select Hero Class" }; diff --git a/OpenDiablo2.Scenes/SelectHeroClass.cs b/OpenDiablo2.Scenes/SelectHeroClass.cs index 8c75eb37..636f698a 100644 --- a/OpenDiablo2.Scenes/SelectHeroClass.cs +++ b/OpenDiablo2.Scenes/SelectHeroClass.cs @@ -37,7 +37,6 @@ namespace OpenDiablo2.Scenes private readonly IPaletteProvider paletteProvider; private readonly IMPQProvider mpqProvider; private readonly IMouseInfoProvider mouseInfoProvider; - private readonly IMusicProvider musicProvider; private readonly ISceneManager sceneManager; private readonly ITextDictionary textDictionary; private readonly IKeyboardInfoProvider keyboardInfoProvider; @@ -58,7 +57,6 @@ namespace OpenDiablo2.Scenes IPaletteProvider paletteProvider, IMPQProvider mpqProvider, IMouseInfoProvider mouseInfoProvider, - IMusicProvider musicProvider, ISceneManager sceneManager, Func createButton, Func createTextBox, diff --git a/OpenDiablo2.ServiceBus/SessionManager.cs b/OpenDiablo2.ServiceBus/SessionManager.cs index fec544b2..3a767644 100644 --- a/OpenDiablo2.ServiceBus/SessionManager.cs +++ b/OpenDiablo2.ServiceBus/SessionManager.cs @@ -24,7 +24,6 @@ namespace OpenDiablo2.ServiceBus private RequestSocket requestSocket; private AutoResetEvent resetEvent = new AutoResetEvent(false); private ISessionServer sessionServer; - public Guid PlayerId { get; private set; } private bool running = false; public OnSetSeedEvent OnSetSeed { get; set; }