diff --git a/OpenDiablo2.Common/Interfaces/Data/IMPQProvider.cs b/OpenDiablo2.Common/Interfaces/Data/IMPQProvider.cs index 6ad23c92..0fda4891 100644 --- a/OpenDiablo2.Common/Interfaces/Data/IMPQProvider.cs +++ b/OpenDiablo2.Common/Interfaces/Data/IMPQProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using OpenDiablo2.Common.Models; namespace OpenDiablo2.Common.Interfaces @@ -10,5 +11,7 @@ namespace OpenDiablo2.Common.Interfaces IEnumerable GetMPQs(); IEnumerable GetTextFile(string fileName); Stream GetStream(string fileName); + byte[] GetBytes(string fileName); + void GetBytesAsync(string fileName, Action callback); } } diff --git a/OpenDiablo2.Common/Interfaces/Data/IMusicProvider.cs b/OpenDiablo2.Common/Interfaces/Data/IMusicProvider.cs index dc49d0ab..3fc246ba 100644 --- a/OpenDiablo2.Common/Interfaces/Data/IMusicProvider.cs +++ b/OpenDiablo2.Common/Interfaces/Data/IMusicProvider.cs @@ -3,10 +3,11 @@ using System.IO; namespace OpenDiablo2.Common.Interfaces { - public interface IMusicProvider : IDisposable + public interface ISoundProvider : IDisposable { void LoadSong(Stream data); void PlaySong(); void StopSong(); + void PlaySfx(byte[] data); } } diff --git a/OpenDiablo2.Common/ResourcePaths.cs b/OpenDiablo2.Common/ResourcePaths.cs index 167f6232..91f650f6 100644 --- a/OpenDiablo2.Common/ResourcePaths.cs +++ b/OpenDiablo2.Common/ResourcePaths.cs @@ -158,6 +158,27 @@ namespace OpenDiablo2.Common public const string Experience = @"data\global\excel\experience.txt"; public const string CharStats = @"data\global\excel\charstats.txt"; + // --- Music --- + public const string BGMTitle = @"data\global\music\introedit.wav"; + public const string BGMTown1 = @"data\global\music\Act1\town1.wav"; + + // --- Sound Effects --- + public const string SFXButtonClick = @"data\global\sfx\Cursor\button.wav"; + public const string SFXAmazonDeselect = @"data\global\sfx\Cursor\intro\amazon deselect.wav"; + public const string SFXAmazonSelect = @"data\global\sfx\Cursor\intro\amazon select.wav"; + public const string SFXAssassinDeselect = @"data\global\sfx\Cursor\intro\assassin deselect.wav"; + public const string SFXAssassinSelect = @"data\global\sfx\Cursor\intro\assassin select.wav"; + public const string SFXBarbarianDeselect = @"data\global\sfx\Cursor\intro\barbarian deselect.wav"; + public const string SFXBarbarianSelect = @"data\global\sfx\Cursor\intro\barbarian select.wav"; + public const string SFXDruidDeselect = @"data\global\sfx\Cursor\intro\druid deselect.wav"; + public const string SFXDruidSelect = @"data\global\sfx\Cursor\intro\druid select.wav"; + public const string SFXNecromancerDeselect = @"data\global\sfx\Cursor\intro\necromancer deselect.wav"; + public const string SFXNecromancerSelect = @"data\global\sfx\Cursor\intro\necromancer select.wav"; + public const string SFXPaladinDeselect = @"data\global\sfx\Cursor\intro\paladin deselect.wav"; + public const string SFXPaladinSelect = @"data\global\sfx\Cursor\intro\paladin select.wav"; + public const string SFXSorceressDeselect = @"data\global\sfx\Cursor\intro\sorceress deselect.wav"; + public const string SFXSorceressSelect = @"data\global\sfx\Cursor\intro\sorceress select.wav"; + public static string GeneratePathForItem(string spriteName) { return $@"data\global\items\{spriteName}.dc6"; diff --git a/OpenDiablo2.Core/MPQProvider.cs b/OpenDiablo2.Core/MPQProvider.cs index 004a5417..3fe9ada1 100644 --- a/OpenDiablo2.Core/MPQProvider.cs +++ b/OpenDiablo2.Core/MPQProvider.cs @@ -51,6 +51,22 @@ namespace OpenDiablo2.Core } } + public byte[] GetBytes(string fileName) + { + var stream = GetStream(fileName); + var result = new byte[stream.Length]; + stream.Read(result, 0, (int)stream.Length); + return result; + } + + public void GetBytesAsync(string fileName, Action callback) + { + var stream = GetStream(fileName); + var result = new byte[stream.Length]; + stream.Read(result, 0, (int)stream.Length); + callback(result); + } + public IEnumerable GetMPQs() => mpqs; public Stream GetStream(string fileName) diff --git a/OpenDiablo2.Core/UI/Button.cs b/OpenDiablo2.Core/UI/Button.cs index 2610c1c4..ad8d6396 100644 --- a/OpenDiablo2.Core/UI/Button.cs +++ b/OpenDiablo2.Core/UI/Button.cs @@ -10,7 +10,9 @@ namespace OpenDiablo2.Core.UI { private readonly IMouseInfoProvider mouseInfoProvider; private readonly IRenderWindow renderWindow; + private readonly ISoundProvider musicProvider; private readonly ButtonLayout buttonLayout; + private readonly byte[] sfxButtonClick; public OnActivateDelegate OnActivate { get; set; } public OnToggleDelegate OnToggle { get; set; } @@ -67,12 +69,15 @@ namespace OpenDiablo2.Core.UI public Button( ButtonLayout buttonLayout, IRenderWindow renderWindow, - IMouseInfoProvider mouseInfoProvider + IMouseInfoProvider mouseInfoProvider, + ISoundProvider soundProvider, + IMPQProvider mpqProvider ) { this.buttonLayout = buttonLayout; this.renderWindow = renderWindow; this.mouseInfoProvider = mouseInfoProvider; + this.musicProvider = soundProvider; font = renderWindow.LoadFont(ResourcePaths.FontExocet10, Palettes.Units); label = renderWindow.CreateLabel(font); @@ -91,6 +96,8 @@ namespace OpenDiablo2.Core.UI label.MaxWidth = buttonWidth - 8; label.Alignment = Common.Enums.eTextAlign.Centered; + + sfxButtonClick = mpqProvider.GetBytes(ResourcePaths.SFXButtonClick); } public bool Toggle() @@ -138,7 +145,7 @@ namespace OpenDiablo2.Core.UI // The button is being pressed down mouseInfoProvider.ReserveMouse = true; active = true; - + musicProvider.PlaySfx(sfxButtonClick); } else if (active && !mouseInfoProvider.LeftMouseDown) { diff --git a/OpenDiablo2.SDL2/AutofacModule.cs b/OpenDiablo2.SDL2/AutofacModule.cs index 49498474..e26d37cc 100644 --- a/OpenDiablo2.SDL2/AutofacModule.cs +++ b/OpenDiablo2.SDL2/AutofacModule.cs @@ -13,7 +13,7 @@ namespace OpenDiablo2.SDL2_ log.Info("Configuring OpenDiablo2.Core service implementations."); builder.RegisterType().AsImplementedInterfaces().SingleInstance(); - builder.RegisterType().AsImplementedInterfaces().SingleInstance(); + builder.RegisterType().AsImplementedInterfaces().SingleInstance(); } } } diff --git a/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj b/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj index f0fe463a..ba59f093 100644 --- a/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj +++ b/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj @@ -62,7 +62,7 @@ - + diff --git a/OpenDiablo2.SDL2/SDL2MusicPlayer.cs b/OpenDiablo2.SDL2/SDL2MusicProvider.cs similarity index 69% rename from OpenDiablo2.SDL2/SDL2MusicPlayer.cs rename to OpenDiablo2.SDL2/SDL2MusicProvider.cs index ee9d547f..3f385302 100644 --- a/OpenDiablo2.SDL2/SDL2MusicPlayer.cs +++ b/OpenDiablo2.SDL2/SDL2MusicProvider.cs @@ -16,18 +16,20 @@ using System; using System.IO; +using System.Threading; using OpenDiablo2.Common.Interfaces; using SDL2; namespace OpenDiablo2.SDL2_ { - public sealed class SDL2MusicPlayer : IMusicProvider + public sealed class SDL2MusicProvider : ISoundProvider { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private IntPtr music = IntPtr.Zero; private int musicChannel; + private byte[] musicBytes; // Cannot be local or GC will destory it with great anger - public SDL2MusicPlayer() + public SDL2MusicProvider() { if (SDL_mixer.Mix_OpenAudio(22050, SDL_mixer.MIX_DEFAULT_FORMAT, 2, 1024) < 0) log.Error($"SDL_mixer could not initialize! SDL_mixer Error: {SDL.SDL_GetError()}"); @@ -35,6 +37,9 @@ namespace OpenDiablo2.SDL2_ public void PlaySong() { + if (music == IntPtr.Zero) + return; + musicChannel = SDL_mixer.Mix_PlayChannel(-1, music, 1); } @@ -43,9 +48,14 @@ namespace OpenDiablo2.SDL2_ if (music != IntPtr.Zero) StopSong(); - var br = new BinaryReader(data); - var bytes = br.ReadBytes((int)(data.Length - data.Position)); - music = SDL_mixer.Mix_QuickLoad_WAV(bytes); + musicBytes = new byte[data.Length - data.Position]; + data.ReadAsync(musicBytes, 0, (int)(data.Length - data.Position)); + + // Wait until SOMETHING gets written out + while (musicBytes[8] == 0) + Thread.Sleep(1); + + music = SDL_mixer.Mix_QuickLoad_WAV(musicBytes); } public void StopSong() @@ -54,6 +64,7 @@ namespace OpenDiablo2.SDL2_ return; SDL_mixer.Mix_HaltChannel(musicChannel); SDL_mixer.Mix_FreeChunk(music); + music = IntPtr.Zero; } public void Dispose() @@ -62,6 +73,10 @@ namespace OpenDiablo2.SDL2_ SDL_mixer.Mix_CloseAudio(); } - + public void PlaySfx(byte[] data) + { + var sound = SDL_mixer.Mix_QuickLoad_WAV(data); + SDL_mixer.Mix_PlayChannel(-1, sound, 0); + } } } diff --git a/OpenDiablo2.Scenes/Game.cs b/OpenDiablo2.Scenes/Game.cs index c20ad3c1..a8c605f0 100644 --- a/OpenDiablo2.Scenes/Game.cs +++ b/OpenDiablo2.Scenes/Game.cs @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +using OpenDiablo2.Common; using OpenDiablo2.Common.Attributes; using OpenDiablo2.Common.Enums; using OpenDiablo2.Common.Interfaces; @@ -43,6 +44,8 @@ namespace OpenDiablo2.Scenes IMouseInfoProvider mouseInfoProvider, IItemManager itemManager, ISessionManager sessionManager, + ISoundProvider soundProvider, + IMPQProvider mpqProvider, IGameHUD gameHUD ) { @@ -53,6 +56,10 @@ namespace OpenDiablo2.Scenes this.sessionManager = sessionManager; this.gameHUD = gameHUD; + // TODO: Dynamic based on actual location + soundProvider.StopSong(); + soundProvider.LoadSong(mpqProvider.GetStream(ResourcePaths.BGMTown1)); + soundProvider.PlaySong(); //var item = itemManager.getItem("hdm"); } diff --git a/OpenDiablo2.Scenes/MainMenu.cs b/OpenDiablo2.Scenes/MainMenu.cs index c943b575..9561959e 100644 --- a/OpenDiablo2.Scenes/MainMenu.cs +++ b/OpenDiablo2.Scenes/MainMenu.cs @@ -40,7 +40,7 @@ namespace OpenDiablo2.Scenes IRenderWindow renderWindow, ISceneManager sceneManager, IResourceManager resourceManager, - IMusicProvider musicProvider, + ISoundProvider soundProvider, IMPQProvider mpqProvider, Func createButton, Func getScene // Temporary until SDL load functions are sped up @@ -89,8 +89,8 @@ namespace OpenDiablo2.Scenes getScene(scenesToLoad[i]); } - musicProvider.LoadSong(mpqProvider.GetStream("data\\global\\music\\introedit.wav")); - musicProvider.PlaySong(); + soundProvider.LoadSong(mpqProvider.GetStream(ResourcePaths.BGMTitle)); + soundProvider.PlaySong(); } private void OnVisitWebsiteClicked() diff --git a/OpenDiablo2.Scenes/SelectHeroClass.cs b/OpenDiablo2.Scenes/SelectHeroClass.cs index 0308f7c4..d59be1f6 100644 --- a/OpenDiablo2.Scenes/SelectHeroClass.cs +++ b/OpenDiablo2.Scenes/SelectHeroClass.cs @@ -37,6 +37,7 @@ namespace OpenDiablo2.Scenes private readonly ITextDictionary textDictionary; private readonly IKeyboardInfoProvider keyboardInfoProvider; private readonly IGameState gameState; + private readonly ISoundProvider soundProvider; private bool showEntryUi = false; private eHero? selectedHero = null; @@ -50,14 +51,20 @@ namespace OpenDiablo2.Scenes private readonly ITextBox characterNameTextBox; private readonly Dictionary heroRenderInfo = new Dictionary(); + private byte[] sfxAmazonSelect, sfxAmazonDeselect, sfxAssassinSelect, sfxAssassinDeselect, sfxBarbarianSelect, sfxBarbarianDeselect, + sfxDruidSelect, sfxDruidDeselect, sfxNecromancerSelect, sfxNecromancerDeselect, sfxPaladinSelect, sfxPaladinDeselect, + sfxSorceressSelect, sfxSorceressDeselect; + public SelectHeroClass( IRenderWindow renderWindow, IMouseInfoProvider mouseInfoProvider, ISceneManager sceneManager, + ISoundProvider soundProvider, Func createButton, Func createTextBox, ITextDictionary textDictionary, IKeyboardInfoProvider keyboardInfoProvider, + IMPQProvider mpqProvider, IGameState gameState ) { @@ -66,6 +73,7 @@ namespace OpenDiablo2.Scenes this.sceneManager = sceneManager; this.textDictionary = textDictionary; this.keyboardInfoProvider = keyboardInfoProvider; + this.soundProvider = soundProvider; this.gameState = gameState; @@ -83,7 +91,7 @@ namespace OpenDiablo2.Scenes SelectedSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelectBarbarianSelected, Palettes.Fechar, new Point(400, 330)), BackWalkSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelectBarbarianBackWalk, Palettes.Fechar, new Point(400, 330)), SelectionBounds = new Rectangle(364, 201, 90, 170), - ForwardWalkTimeMs = 3000, + ForwardWalkTimeMs = 2500, BackWalkTimeMs = 1000 }; @@ -99,7 +107,7 @@ namespace OpenDiablo2.Scenes BackWalkSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelecSorceressBackWalk, Palettes.Fechar, new Point(626, 352)), BackWalkSpriteOverlay = renderWindow.LoadSprite(ResourcePaths.CharacterSelecSorceressBackWalkOverlay, Palettes.Fechar, new Point(626, 352)), SelectionBounds = new Rectangle(580, 240, 65, 160), - ForwardWalkTimeMs = 3000, + ForwardWalkTimeMs = 2300, BackWalkTimeMs = 1200 }; heroRenderInfo[eHero.Sorceress].SelectedSpriteOverlay.Blend = true; @@ -136,7 +144,7 @@ namespace OpenDiablo2.Scenes SelectedSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelecPaladinSelected, Palettes.Fechar, new Point(521, 338)), BackWalkSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelecPaladinBackWalk, Palettes.Fechar, new Point(521, 338)), SelectionBounds = new Rectangle(490, 210, 65, 180), - ForwardWalkTimeMs = 4000, + ForwardWalkTimeMs = 3400, BackWalkTimeMs = 1300 }; @@ -150,7 +158,7 @@ namespace OpenDiablo2.Scenes SelectedSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelecAmazonSelected, Palettes.Fechar, new Point(100, 339)), BackWalkSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelecAmazonBackWalk, Palettes.Fechar, new Point(100, 339)), SelectionBounds = new Rectangle(70, 220, 55, 200), - ForwardWalkTimeMs = 2600, + ForwardWalkTimeMs = 2200, BackWalkTimeMs = 1500 }; @@ -163,7 +171,7 @@ namespace OpenDiablo2.Scenes SelectedSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelectAssassinSelected, Palettes.Fechar, new Point(231, 365)), BackWalkSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelectAssassinBackWalk, Palettes.Fechar, new Point(231, 365)), SelectionBounds = new Rectangle(175, 235, 50, 180), - ForwardWalkTimeMs = 3000, + ForwardWalkTimeMs = 3800, BackWalkTimeMs = 1500 }; @@ -176,7 +184,7 @@ namespace OpenDiablo2.Scenes SelectedSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelectDruidSelected, Palettes.Fechar, new Point(720, 370)), BackWalkSprite = renderWindow.LoadSprite(ResourcePaths.CharacterSelectDruidBackWalk, Palettes.Fechar, new Point(720, 370)), SelectionBounds = new Rectangle(680, 220, 70, 195), - ForwardWalkTimeMs = 3000, + ForwardWalkTimeMs = 4800, BackWalkTimeMs = 1500 }; @@ -216,6 +224,21 @@ namespace OpenDiablo2.Scenes characterNameTextBox.Text = ""; characterNameTextBox.Location = new Point(320, 493); + mpqProvider.GetBytesAsync(ResourcePaths.SFXAmazonSelect, x => sfxAmazonSelect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXAmazonDeselect, x => sfxAmazonDeselect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXAssassinSelect, x => sfxAssassinSelect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXAssassinDeselect, x => sfxAssassinDeselect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXBarbarianSelect, x => sfxBarbarianSelect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXBarbarianDeselect, x => sfxBarbarianDeselect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXDruidSelect, x => sfxDruidSelect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXDruidDeselect, x => sfxDruidDeselect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXNecromancerSelect, x => sfxNecromancerSelect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXNecromancerDeselect, x => sfxNecromancerDeselect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXPaladinSelect, x => sfxPaladinSelect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXPaladinDeselect, x => sfxPaladinDeselect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXSorceressSelect, x => sfxSorceressSelect = x); + mpqProvider.GetBytesAsync(ResourcePaths.SFXSorceressDeselect, x => sfxSorceressDeselect = x); + } private void OnOkclicked() @@ -432,6 +455,7 @@ namespace OpenDiablo2.Scenes selectedHero = hero; UpdateHeroText(); + PlayHeroSelected(hero); return; } @@ -446,6 +470,66 @@ namespace OpenDiablo2.Scenes } + private void PlayHeroSelected(eHero hero) + { + switch (hero) + { + case eHero.Barbarian: + soundProvider.PlaySfx(sfxBarbarianSelect); + break; + case eHero.Necromancer: + soundProvider.PlaySfx(sfxNecromancerSelect); + break; + case eHero.Paladin: + soundProvider.PlaySfx(sfxPaladinSelect); + break; + case eHero.Assassin: + soundProvider.PlaySfx(sfxAssassinSelect); + break; + case eHero.Sorceress: + soundProvider.PlaySfx(sfxSorceressSelect); + break; + case eHero.Amazon: + soundProvider.PlaySfx(sfxAmazonSelect); + break; + case eHero.Druid: + soundProvider.PlaySfx(sfxDruidSelect); + break; + default: + break; + } + } + + private void PlayHeroDeselected(eHero hero) + { + switch (hero) + { + case eHero.Barbarian: + soundProvider.PlaySfx(sfxBarbarianDeselect); + break; + case eHero.Necromancer: + soundProvider.PlaySfx(sfxNecromancerDeselect); + break; + case eHero.Paladin: + soundProvider.PlaySfx(sfxPaladinDeselect); + break; + case eHero.Assassin: + soundProvider.PlaySfx(sfxAssassinDeselect); + break; + case eHero.Sorceress: + soundProvider.PlaySfx(sfxSorceressDeselect); + break; + case eHero.Amazon: + soundProvider.PlaySfx(sfxAmazonDeselect); + break; + case eHero.Druid: + soundProvider.PlaySfx(sfxDruidDeselect); + break; + default: + break; + } + } + private void SetDescLabels(string descKey) { var heroDesc = textDictionary.Translate(descKey);