From 3d98be98fe83766ab267886f23549402eb4c7ba1 Mon Sep 17 00:00:00 2001 From: Tim Sarbin Date: Thu, 22 Nov 2018 16:22:39 -0500 Subject: [PATCH] Added font rendering and label support --- OpenDiablo2.Common/Interfaces/IFont.cs | 3 +- OpenDiablo2.Common/Interfaces/ILabel.cs | 17 +++ .../Interfaces/IRenderWindow.cs | 6 + OpenDiablo2.Common/Models/MPQFont.cs | 43 ++++++ OpenDiablo2.Common/Models/Palette.cs | 3 +- OpenDiablo2.Common/OpenDiablo2.Common.csproj | 2 + OpenDiablo2.Common/ResourcePaths.cs | 7 + OpenDiablo2.Core/GameEngine.cs | 2 +- OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj | 2 + OpenDiablo2.SDL2/SDL2Font.cs | 34 +++++ OpenDiablo2.SDL2/SDL2Label.cs | 123 ++++++++++++++++++ OpenDiablo2.SDL2/SDL2RenderWindow.cs | 62 ++++++++- OpenDiablo2.SDL2/SDL2Sprite.cs | 8 +- OpenDiablo2.Scenes/MainMenu.cs | 18 ++- 14 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 OpenDiablo2.Common/Interfaces/ILabel.cs create mode 100644 OpenDiablo2.Common/Models/MPQFont.cs create mode 100644 OpenDiablo2.SDL2/SDL2Font.cs create mode 100644 OpenDiablo2.SDL2/SDL2Label.cs diff --git a/OpenDiablo2.Common/Interfaces/IFont.cs b/OpenDiablo2.Common/Interfaces/IFont.cs index 1a790f80..40bc54bc 100644 --- a/OpenDiablo2.Common/Interfaces/IFont.cs +++ b/OpenDiablo2.Common/Interfaces/IFont.cs @@ -6,7 +6,8 @@ using System.Threading.Tasks; namespace OpenDiablo2.Common.Interfaces { - public interface IFont + public interface IFont : IDisposable { + } } diff --git a/OpenDiablo2.Common/Interfaces/ILabel.cs b/OpenDiablo2.Common/Interfaces/ILabel.cs new file mode 100644 index 00000000..51818c4c --- /dev/null +++ b/OpenDiablo2.Common/Interfaces/ILabel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Interfaces +{ + public interface ILabel : IDisposable + { + string Text { get; set; } + Point Position { get; set; } + Size TextArea { get; set; } + Color TextColor { get; set; } + } +} diff --git a/OpenDiablo2.Common/Interfaces/IRenderWindow.cs b/OpenDiablo2.Common/Interfaces/IRenderWindow.cs index 97b78ddf..208144ce 100644 --- a/OpenDiablo2.Common/Interfaces/IRenderWindow.cs +++ b/OpenDiablo2.Common/Interfaces/IRenderWindow.cs @@ -17,8 +17,14 @@ namespace OpenDiablo2.Common.Interfaces void Sync(); ISprite LoadSprite(string resourcePath, string palette, Point location); ISprite LoadSprite(string resourcePath, string palette); + IFont LoadFont(string resourcePath, string palette); + ILabel CreateLabel(IFont font); + ILabel CreateLabel(IFont font, string text); + ILabel CreateLabel(IFont font, Point position, string text); void Draw(ISprite sprite); + void Draw(ISprite sprite, Point location); void Draw(ISprite sprite, int frame); void Draw(ISprite sprite, int xSegments, int ySegments, int offset); + void Draw(ILabel label); } } diff --git a/OpenDiablo2.Common/Models/MPQFont.cs b/OpenDiablo2.Common/Models/MPQFont.cs new file mode 100644 index 00000000..f13eabf4 --- /dev/null +++ b/OpenDiablo2.Common/Models/MPQFont.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.Common.Models +{ + public sealed class MPQFont + { + public ImageSet FontImageSet; + public Dictionary CharacterMetric = new Dictionary(); + + public static MPQFont LoadFromStream(Stream fontStream, Stream tableStream) + { + var result = new MPQFont + { + FontImageSet = ImageSet.LoadFromStream(fontStream) + }; + + var br = new BinaryReader(tableStream); + var wooCheck = Encoding.UTF8.GetString(br.ReadBytes(4)); + if (wooCheck != "Woo!") + throw new ApplicationException("Error loading font. Missing the Woo!"); + br.ReadBytes(8); + + while (tableStream.Position < tableStream.Length) + { + br.ReadBytes(3); + var size = new Size(br.ReadByte(), br.ReadByte()); + br.ReadBytes(3); + var charCode = br.ReadByte(); + result.CharacterMetric[charCode] = size; + br.ReadBytes(5); + + } + + return result; + } + } +} diff --git a/OpenDiablo2.Common/Models/Palette.cs b/OpenDiablo2.Common/Models/Palette.cs index 19cba0cb..be391996 100644 --- a/OpenDiablo2.Common/Models/Palette.cs +++ b/OpenDiablo2.Common/Models/Palette.cs @@ -19,10 +19,11 @@ namespace OpenDiablo2.Common.Models public string Name { get; set; } public PaletteEntry[] Colors; - public static Palette LoadFromStream(Stream stream) + public static Palette LoadFromStream(Stream stream, string paletteName) { var result = new Palette { + Name = paletteName, Colors = new PaletteEntry[256] }; diff --git a/OpenDiablo2.Common/OpenDiablo2.Common.csproj b/OpenDiablo2.Common/OpenDiablo2.Common.csproj index d4824bb1..bc99de4b 100644 --- a/OpenDiablo2.Common/OpenDiablo2.Common.csproj +++ b/OpenDiablo2.Common/OpenDiablo2.Common.csproj @@ -70,6 +70,7 @@ + @@ -80,6 +81,7 @@ + diff --git a/OpenDiablo2.Common/ResourcePaths.cs b/OpenDiablo2.Common/ResourcePaths.cs index 8156f384..ebbcb350 100644 --- a/OpenDiablo2.Common/ResourcePaths.cs +++ b/OpenDiablo2.Common/ResourcePaths.cs @@ -20,5 +20,12 @@ namespace OpenDiablo2.Common // --- Mouse Pointers --- public static string CursorDefault = "data\\global\\ui\\CURSOR\\ohand.DC6"; + + // --- Fonts --- + public static string Font8 = "data\\local\\font\\latin\\font8"; + public static string Font16 = "data\\local\\font\\latin\\font16"; + public static string FontFormal12 = "data\\local\\font\\latin\\fontformal12"; + public static string FontFormal10 = "data\\local\\font\\latin\\fontformal10"; + } } diff --git a/OpenDiablo2.Core/GameEngine.cs b/OpenDiablo2.Core/GameEngine.cs index 5c70be0f..98282292 100644 --- a/OpenDiablo2.Core/GameEngine.cs +++ b/OpenDiablo2.Core/GameEngine.cs @@ -44,7 +44,7 @@ namespace OpenDiablo2.Core { var paletteNameParts = paletteFile.Split('\\'); var paletteName = paletteNameParts[paletteNameParts.Count() - 2]; - PaletteTable[paletteName] = Palette.LoadFromStream(mpqProvider.GetStream(paletteFile)); + PaletteTable[paletteName] = Palette.LoadFromStream(mpqProvider.GetStream(paletteFile), paletteName); } } diff --git a/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj b/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj index c3665410..8500aeb7 100644 --- a/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj +++ b/OpenDiablo2.SDL2/OpenDiablo2.SDL2.csproj @@ -75,6 +75,8 @@ + + diff --git a/OpenDiablo2.SDL2/SDL2Font.cs b/OpenDiablo2.SDL2/SDL2Font.cs new file mode 100644 index 00000000..6f32377a --- /dev/null +++ b/OpenDiablo2.SDL2/SDL2Font.cs @@ -0,0 +1,34 @@ +using OpenDiablo2.Common.Interfaces; +using OpenDiablo2.Common.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.SDL2_ +{ + internal sealed class SDL2Font : IFont + { + internal readonly MPQFont font; + internal readonly SDL2Sprite sprite; + + public Palette CurrentPalette + { + get => sprite.CurrentPalette; + set => sprite.CurrentPalette = value; + } + + internal SDL2Font(MPQFont font, IntPtr renderer) + { + this.font = font; + + sprite = new SDL2Sprite(font.FontImageSet, renderer); + } + + public void Dispose() + { + sprite.Dispose(); + } + } +} diff --git a/OpenDiablo2.SDL2/SDL2Label.cs b/OpenDiablo2.SDL2/SDL2Label.cs new file mode 100644 index 00000000..313fb68f --- /dev/null +++ b/OpenDiablo2.SDL2/SDL2Label.cs @@ -0,0 +1,123 @@ +using OpenDiablo2.Common.Interfaces; +using SDL2; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace OpenDiablo2.SDL2_ +{ + internal sealed class SDL2Label : ILabel + { + private readonly SDL2Font font; + private readonly IntPtr renderer; + internal IntPtr texture; + internal Size textureSize = new Size(); + public Point Position { get; set; } + public Size TextArea { get; set; } = new Size(); + + private Color textColor = Color.White; + public Color TextColor + { + get => textColor; + set + { + textColor = value; + RegenerateTexture(); + } + } + + private string text; + public string Text + { + get => text; + set + { + text = value; + RegenerateTexture(); + } + } + + internal SDL2Label(IFont font, IntPtr renderer) + { + this.renderer = renderer; + this.font = font as SDL2Font; + texture = IntPtr.Zero; + } + + internal Size CalculateSize() + { + int w = 0; + int h = 0; + foreach (var ch in text) + { + var metric = font.font.CharacterMetric[(byte)ch]; + w += metric.Width; + h = Math.Max(h, metric.Height); + } + + return new Size(w, h); + } + + internal int Pow2(int input) + { + var result = 1; + while (result < input) + result *= 2; + return result; + } + + internal void RegenerateTexture() + { + if (texture != IntPtr.Zero) + SDL.SDL_DestroyTexture(texture); + + TextArea = CalculateSize(); + textureSize = new Size(Pow2(TextArea.Width), Pow2(TextArea.Height)); + texture = SDL.SDL_CreateTexture(renderer, SDL.SDL_PIXELFORMAT_ARGB8888, (int)SDL.SDL_TextureAccess.SDL_TEXTUREACCESS_TARGET, textureSize.Width, textureSize.Height); + + if (texture == IntPtr.Zero) + throw new ApplicationException("Unaple to initialize texture."); + + SDL.SDL_SetTextureBlendMode(texture, SDL.SDL_BlendMode.SDL_BLENDMODE_BLEND); + SDL.SDL_SetRenderTarget(renderer, texture); + SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); + SDL.SDL_RenderFillRect(renderer, IntPtr.Zero); + SDL.SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); + + int cx = 0; + int cy = 0; + foreach (var ch in text) + { + WriteCharacter(cx, cy, (byte)ch); + cx += font.font.CharacterMetric[(byte)ch].Width; + } + + SDL.SDL_SetRenderTarget(renderer, IntPtr.Zero); + } + + internal void WriteCharacter(int cx, int cy, byte character) + { + var rect = new SDL.SDL_Rect + { + x = cx, + y = cy, + w = font.sprite.FrameSize.Width, + h = font.sprite.FrameSize.Height + }; + + SDL.SDL_SetTextureColorMod(font.sprite.textures[character], TextColor.R, TextColor.G, TextColor.B); + SDL.SDL_RenderCopy(renderer, font.sprite.textures[character], IntPtr.Zero, ref rect); + + } + + public void Dispose() + { + if (texture != IntPtr.Zero) + SDL.SDL_DestroyTexture(texture); + } + } +} diff --git a/OpenDiablo2.SDL2/SDL2RenderWindow.cs b/OpenDiablo2.SDL2/SDL2RenderWindow.cs index d094be81..b6e36f4b 100644 --- a/OpenDiablo2.SDL2/SDL2RenderWindow.cs +++ b/OpenDiablo2.SDL2/SDL2RenderWindow.cs @@ -92,6 +92,11 @@ namespace OpenDiablo2.SDL2_ } } + public void Draw(ISprite sprite, Point location) + { + sprite.Location = location; + Draw(sprite); + } public void Draw(ISprite sprite, int frame) { @@ -143,10 +148,61 @@ namespace OpenDiablo2.SDL2_ public ISprite LoadSprite(string resourcePath, string palette) => LoadSprite(resourcePath, palette, Point.Empty); public ISprite LoadSprite(string resourcePath, string palette, Point location) { - var result = new SDL2Sprite(ImageSet.LoadFromStream(mpqProvider.GetStream(resourcePath)), renderer); - result.CurrentPalette = paletteProvider.PaletteTable[palette]; - result.Location = location; + var result = new SDL2Sprite(ImageSet.LoadFromStream(mpqProvider.GetStream(resourcePath)), renderer) + { + CurrentPalette = paletteProvider.PaletteTable[palette], + Location = location + }; return result; } + + public IFont LoadFont(string resourcePath, string palette) + { + var result = new SDL2Font(MPQFont.LoadFromStream(mpqProvider.GetStream($"{resourcePath}.DC6"), mpqProvider.GetStream($"{resourcePath}.tbl")), renderer) + { + CurrentPalette = paletteProvider.PaletteTable[palette] + }; + return result; + } + + + public ILabel CreateLabel(IFont font) + { + var result = new SDL2Label(font, renderer); + return result; + } + + public ILabel CreateLabel(IFont font, string text) + { + var result = CreateLabel(font); + result.Text = text; + return result; + } + + public ILabel CreateLabel(IFont font, Point position, string text) + { + var result = new SDL2Label(font, renderer) + { + Text = text, + Position = position + }; + + return result; + } + + public void Draw(ILabel label) + { + var lbl = label as SDL2Label; + var loc = lbl.Position; + + var destRect = new SDL.SDL_Rect + { + x = loc.X, + y = loc.Y, + w = lbl.textureSize.Width, + h = lbl.textureSize.Height + }; + SDL.SDL_RenderCopy(renderer, lbl.texture, IntPtr.Zero, ref destRect); + } } } diff --git a/OpenDiablo2.SDL2/SDL2Sprite.cs b/OpenDiablo2.SDL2/SDL2Sprite.cs index 9b3244f1..70561582 100644 --- a/OpenDiablo2.SDL2/SDL2Sprite.cs +++ b/OpenDiablo2.SDL2/SDL2Sprite.cs @@ -12,7 +12,7 @@ using OpenDiablo2.Common.Models; namespace OpenDiablo2.SDL2_ { - public sealed class SDL2Sprite : ISprite + internal sealed class SDL2Sprite : ISprite { public Point Location { get; set; } = new Point(); public Size FrameSize { get; set; } = new Size(); @@ -71,9 +71,9 @@ namespace OpenDiablo2.SDL2_ private Color AdjustColor(Color source) => Color.FromArgb( source.A, - (byte)Math.Min((float)source.R * 1.0, 255), - (byte)Math.Min((float)source.G * 1.0, 255), - (byte)Math.Min((float)source.B * 1.0, 255) + (byte)Math.Min((float)source.R * 1.2, 255), + (byte)Math.Min((float)source.G * 1.2, 255), + (byte)Math.Min((float)source.B * 1.2, 255) ); private IntPtr LoadFrame(ImageFrame frame, IntPtr renderer) diff --git a/OpenDiablo2.Scenes/MainMenu.cs b/OpenDiablo2.Scenes/MainMenu.cs index 4056c90d..edb11761 100644 --- a/OpenDiablo2.Scenes/MainMenu.cs +++ b/OpenDiablo2.Scenes/MainMenu.cs @@ -25,6 +25,8 @@ namespace OpenDiablo2.Scenes private float logoFrame; private ISprite backgroundSprite, diabloLogoLeft, diabloLogoRight, diabloLogoLeftBlack, diabloLogoRightBlack, mouseSprite, wideButton; + private IFont labelFont; + private ILabel versionLabel, urlLabel; public MainMenu( IRenderWindow renderWindow, @@ -48,6 +50,10 @@ namespace OpenDiablo2.Scenes mouseSprite = renderWindow.LoadSprite(ResourcePaths.CursorDefault, Palettes.Units); wideButton = renderWindow.LoadSprite("data\\global\\ui\\FrontEnd\\WideButtonBlank.dc6", "ACT1"); + labelFont = renderWindow.LoadFont(ResourcePaths.Font16, Palettes.Static); + versionLabel = renderWindow.CreateLabel(labelFont, new Point(50, 555), "v0.01 Pre-Alpha"); + urlLabel = renderWindow.CreateLabel(labelFont, new Point(50, 569), "https://github.com/essial/OpenDiablo2/"); + urlLabel.TextColor = Color.Magenta; var loadingSprite = renderWindow.LoadSprite(ResourcePaths.LoadingScreen, Palettes.Loading, new Point(300, 400)); @@ -76,19 +82,25 @@ namespace OpenDiablo2.Scenes { renderWindow.Clear(); + // Render the background renderWindow.Draw(backgroundSprite, 4, 3, 0); + // Render the flaming diablo 2 logo renderWindow.Draw(diabloLogoLeftBlack, (int)((float)diabloLogoLeftBlack.TotalFrames * logoFrame)); renderWindow.Draw(diabloLogoRightBlack, (int)((float)diabloLogoRightBlack.TotalFrames * logoFrame)); - renderWindow.Draw(diabloLogoLeft, (int)((float)diabloLogoLeft.TotalFrames * logoFrame)); renderWindow.Draw(diabloLogoRight, (int)((float)diabloLogoRight.TotalFrames * logoFrame)); + // Render the text + renderWindow.Draw(versionLabel); + renderWindow.Draw(urlLabel); + + // Render the UI buttons wideButton.Location = new Point(264, 290); renderWindow.Draw(wideButton, 2, 1, 0); - mouseSprite.Location = new Point(mouseInfoProvider.MouseX, mouseInfoProvider.MouseY + mouseSprite.FrameSize.Height - 1); - renderWindow.Draw(mouseSprite); + // Draw the mouse + renderWindow.Draw(mouseSprite, new Point(mouseInfoProvider.MouseX, mouseInfoProvider.MouseY + 3)); renderWindow.Sync(); }