mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-04 07:37:48 -05:00
3731e631cf
* fixed #969
515 lines
14 KiB
Go
515 lines
14 KiB
Go
package d2systems
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2bitmapfont"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2button"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
|
"github.com/gravestench/akara"
|
|
"image/color"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
fontCacheBudget = 64
|
|
)
|
|
|
|
// NewWidgetFactory creates a new ui widget factory which is intended
|
|
// to be embedded in the game object factory system.
|
|
func NewUIWidgetFactory(
|
|
b akara.BaseSystem,
|
|
l *d2util.Logger,
|
|
spriteFactory *SpriteFactory,
|
|
shapeFactory *ShapeSystem,
|
|
) *UIWidgetFactory {
|
|
sys := &UIWidgetFactory{
|
|
Logger: l,
|
|
SpriteFactory: spriteFactory,
|
|
ShapeSystem: shapeFactory,
|
|
bitmapFontCache: d2cache.CreateCache(fontCacheBudget),
|
|
buttonLoadQueue: make(buttonLoadQueue),
|
|
labelLoadQueue: make(labelLoadQueue),
|
|
}
|
|
|
|
sys.BaseSystem = b
|
|
|
|
sys.World.AddSystem(sys)
|
|
|
|
return sys
|
|
}
|
|
|
|
type buttonLoadQueueEntry struct {
|
|
label, sprite akara.EID
|
|
}
|
|
|
|
type buttonLoadQueue = map[akara.EID]buttonLoadQueueEntry
|
|
|
|
type labelLoadQueueEntry struct {
|
|
table, sprite akara.EID
|
|
}
|
|
|
|
type labelLoadQueue = map[akara.EID]labelLoadQueueEntry
|
|
|
|
// UIWidgetFactory is responsible for creating UI widgets like buttons and tabs
|
|
type UIWidgetFactory struct {
|
|
akara.BaseSubscriberSystem
|
|
*d2util.Logger
|
|
*RenderSystem
|
|
*SpriteFactory
|
|
*ShapeSystem
|
|
buttonLoadQueue
|
|
labelLoadQueue
|
|
bitmapFontCache d2interface.Cache
|
|
labelsToUpdate *akara.Subscription
|
|
buttonsToUpdate *akara.Subscription
|
|
booted bool
|
|
Components struct {
|
|
File d2components.FileFactory
|
|
Transform d2components.TransformFactory
|
|
Interactive d2components.InteractiveFactory
|
|
FontTable d2components.FontTableFactory
|
|
Palette d2components.PaletteFactory
|
|
BitmapFont d2components.BitmapFontFactory
|
|
Label d2components.LabelFactory
|
|
Button d2components.ButtonFactory
|
|
Sprite d2components.SpriteFactory
|
|
Color d2components.ColorFactory
|
|
Texture d2components.TextureFactory
|
|
Ready d2components.ReadyFactory
|
|
SceneGraphNode d2components.SceneGraphNodeFactory
|
|
}
|
|
}
|
|
|
|
// Init the ui widget factory, injecting the necessary components
|
|
func (t *UIWidgetFactory) Init(world *akara.World) {
|
|
t.World = world
|
|
|
|
t.Debug("initializing ui widget factory ...")
|
|
|
|
t.setupFactories()
|
|
t.setupSubscriptions()
|
|
}
|
|
|
|
func (t *UIWidgetFactory) setupFactories() {
|
|
t.InjectComponent(&d2components.File{}, &t.Components.File.ComponentFactory)
|
|
t.InjectComponent(&d2components.Transform{}, &t.Components.Transform.ComponentFactory)
|
|
t.InjectComponent(&d2components.Interactive{}, &t.Components.Interactive.ComponentFactory)
|
|
t.InjectComponent(&d2components.FontTable{}, &t.Components.FontTable.ComponentFactory)
|
|
t.InjectComponent(&d2components.Palette{}, &t.Components.Palette.ComponentFactory)
|
|
t.InjectComponent(&d2components.BitmapFont{}, &t.Components.BitmapFont.ComponentFactory)
|
|
t.InjectComponent(&d2components.Label{}, &t.Components.Label.ComponentFactory)
|
|
t.InjectComponent(&d2components.Button{}, &t.Components.Button.ComponentFactory)
|
|
t.InjectComponent(&d2components.Sprite{}, &t.Components.Sprite.ComponentFactory)
|
|
t.InjectComponent(&d2components.Color{}, &t.Components.Color.ComponentFactory)
|
|
t.InjectComponent(&d2components.Ready{}, &t.Components.Ready.ComponentFactory)
|
|
t.InjectComponent(&d2components.Texture{}, &t.Components.Texture.ComponentFactory)
|
|
t.InjectComponent(&d2components.SceneGraphNode{}, &t.Components.SceneGraphNode.ComponentFactory)
|
|
}
|
|
|
|
func (t *UIWidgetFactory) setupSubscriptions() {
|
|
labelsToUpdate := t.NewComponentFilter().
|
|
Require(&d2components.Label{}).
|
|
Require(&d2components.Ready{}).
|
|
Build()
|
|
|
|
buttonsToUpdate := t.NewComponentFilter().
|
|
Require(&d2components.Button{}).
|
|
Require(&d2components.Ready{}).
|
|
Build()
|
|
|
|
t.labelsToUpdate = t.AddSubscription(labelsToUpdate)
|
|
t.buttonsToUpdate = t.AddSubscription(buttonsToUpdate)
|
|
}
|
|
|
|
func (t *UIWidgetFactory) boot() {
|
|
if t.RenderSystem == nil {
|
|
return
|
|
}
|
|
|
|
if t.RenderSystem.renderer == nil {
|
|
return
|
|
}
|
|
|
|
t.booted = true
|
|
}
|
|
|
|
// Update processes the load queues and update the widgets. The load queues are necessary because
|
|
// UI widgets are composed of a bunch of things, which each need to be loaded by other systems (like the asset loader)
|
|
func (t *UIWidgetFactory) Update() {
|
|
start := time.Now()
|
|
|
|
if !t.booted {
|
|
t.boot()
|
|
return
|
|
}
|
|
|
|
for buttonEID := range t.buttonLoadQueue {
|
|
if time.Since(start) > maxTimePerUpdate {
|
|
return
|
|
}
|
|
|
|
t.processButton(buttonEID)
|
|
}
|
|
|
|
for labelEID := range t.labelLoadQueue {
|
|
if time.Since(start) > maxTimePerUpdate {
|
|
return
|
|
}
|
|
|
|
t.processLabel(labelEID)
|
|
}
|
|
|
|
for _, buttonEID := range t.buttonsToUpdate.GetEntities() {
|
|
if time.Since(start) > maxTimePerUpdate {
|
|
return
|
|
}
|
|
|
|
t.updateButton(buttonEID)
|
|
}
|
|
|
|
for _, labelEID := range t.labelsToUpdate.GetEntities() {
|
|
if time.Since(start) > maxTimePerUpdate {
|
|
return
|
|
}
|
|
|
|
t.updateLabel(labelEID)
|
|
}
|
|
}
|
|
|
|
// Label creates a label widget.
|
|
//
|
|
// The font is assumed to be a path for two files, omitting the file extension
|
|
//
|
|
// Basically, diablo2 stored bitmap fonts as two files, a glyph table and sprite.
|
|
//
|
|
// For example, specifying this font: /data/local/FONT/ENG/fontexocet10
|
|
//
|
|
// will use these two files:
|
|
//
|
|
// /data/local/FONT/ENG/fontexocet10.dc6
|
|
//
|
|
// /data/local/FONT/ENG/fontexocet10.tbl
|
|
func (t *UIWidgetFactory) Label(text, font, palettePath string) akara.EID {
|
|
tablePath := font + ".tbl"
|
|
spritePath := font + ".dc6"
|
|
|
|
labelEID := t.NewEntity()
|
|
|
|
tableEID := t.NewEntity()
|
|
t.Components.File.Add(tableEID).Path = tablePath
|
|
|
|
spriteEID := t.SpriteFactory.Sprite(0, 0, spritePath, palettePath)
|
|
|
|
t.labelLoadQueue[labelEID] = labelLoadQueueEntry{
|
|
table: tableEID,
|
|
sprite: spriteEID,
|
|
}
|
|
|
|
label := t.Components.Label.Add(labelEID)
|
|
label.SetText(text)
|
|
|
|
return labelEID
|
|
}
|
|
|
|
func (t *UIWidgetFactory) processLabel(labelEID akara.EID) {
|
|
bmfComponent, found := t.Components.BitmapFont.Get(labelEID)
|
|
if !found {
|
|
t.addBitmapFontForLabel(labelEID)
|
|
return
|
|
}
|
|
|
|
bmfComponent.Sprite.BindRenderer(t.renderer)
|
|
|
|
label, found := t.Components.Label.Get(labelEID)
|
|
if !found {
|
|
label = t.Components.Label.Add(labelEID)
|
|
}
|
|
|
|
label.Font = bmfComponent.BitmapFont
|
|
|
|
t.RemoveEntity(t.labelLoadQueue[labelEID].table)
|
|
|
|
t.Components.Ready.Add(labelEID)
|
|
|
|
delete(t.labelLoadQueue, labelEID)
|
|
}
|
|
|
|
func (t *UIWidgetFactory) updateLabel(labelEID akara.EID) {
|
|
label, found := t.Components.Label.Get(labelEID)
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
bmf, found := t.Components.BitmapFont.Get(labelEID)
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
if label.Font != bmf.BitmapFont {
|
|
label.Font = bmf.BitmapFont
|
|
}
|
|
|
|
col, found := t.Components.Color.Get(labelEID)
|
|
if found {
|
|
label.SetBackgroundColor(col.Color)
|
|
}
|
|
|
|
if !label.IsDirty() {
|
|
return
|
|
}
|
|
|
|
texture, found := t.RenderSystem.Components.Texture.Get(labelEID)
|
|
if !found {
|
|
texture = t.RenderSystem.Components.Texture.Add(labelEID)
|
|
}
|
|
|
|
texture.Texture = t.renderer.NewSurface(label.GetSize())
|
|
|
|
label.Render(texture.Texture)
|
|
}
|
|
|
|
func (t *UIWidgetFactory) addBitmapFontForLabel(labelEID akara.EID) {
|
|
// get the load queue
|
|
entry, found := t.labelLoadQueue[labelEID]
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
// make sure the components have been loaded (by the asset loader)
|
|
_, tableFound := t.Components.FontTable.Get(entry.table)
|
|
_, spriteFound := t.Components.Sprite.Get(entry.sprite)
|
|
|
|
if !(tableFound && spriteFound) {
|
|
return
|
|
}
|
|
|
|
// now we check the cache, see if we can just pull a pre-rendered bitmap font
|
|
tableFile, found := t.Components.File.Get(entry.table)
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
sprite, found := t.Components.Sprite.Get(entry.sprite)
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
cacheKey := fontCacheKey(tableFile.Path, sprite.SpritePath, sprite.PalettePath)
|
|
|
|
if iface, found := t.bitmapFontCache.Retrieve(cacheKey); found {
|
|
// we found it, add the bitmap font component and set the embedded struct to what we retrieved
|
|
t.Components.BitmapFont.Add(labelEID).BitmapFont = iface.(*d2bitmapfont.BitmapFont)
|
|
t.Components.Ready.Add(labelEID)
|
|
|
|
delete(t.labelLoadQueue, labelEID)
|
|
|
|
return
|
|
}
|
|
|
|
bmf := t.createBitmapFont(entry)
|
|
if bmf == nil {
|
|
return
|
|
}
|
|
|
|
// we need to create and cache the bitmap font
|
|
if err := t.bitmapFontCache.Insert(cacheKey, bmf, 1); err != nil {
|
|
t.Warning(err.Error())
|
|
}
|
|
|
|
t.Components.BitmapFont.Add(labelEID).BitmapFont = bmf
|
|
}
|
|
|
|
func (t *UIWidgetFactory) createBitmapFont(entry labelLoadQueueEntry) *d2bitmapfont.BitmapFont {
|
|
// make sure the components have been loaded (by the asset loader)
|
|
table, tableFound := t.Components.FontTable.Get(entry.table)
|
|
sprite, spriteFound := t.Components.Sprite.Get(entry.sprite)
|
|
|
|
if !(tableFound && spriteFound) || sprite.Sprite == nil || table.Data == nil {
|
|
return nil
|
|
}
|
|
|
|
return d2bitmapfont.New(sprite.Sprite, table.Data, color.White)
|
|
}
|
|
|
|
func fontCacheKey(t, s, p string) string {
|
|
return fmt.Sprintf("%s::%s::%s", t, s, p)
|
|
}
|
|
|
|
const isNotSegmented = -1
|
|
|
|
// Button creates a button ui widget
|
|
func (t *UIWidgetFactory) Button(x, y float64, btnType d2button.ButtonType, text string) akara.EID {
|
|
buttonEID := t.NewEntity()
|
|
|
|
button := t.Components.Button.Add(buttonEID)
|
|
|
|
btnTrs := t.Components.Transform.Add(buttonEID)
|
|
btnTrs.Translation.X, btnTrs.Translation.Y = x, y
|
|
|
|
layout := d2button.GetLayout(btnType)
|
|
|
|
button.Layout = layout
|
|
|
|
var labelEID akara.EID
|
|
|
|
if layout.FontPath != "" && layout.PalettePath != "" {
|
|
labelEID = t.Label(text, layout.FontPath, d2resource.PaletteUnits)
|
|
} else {
|
|
// we just make a temporary label and mark it as ready, nothing to do
|
|
labelEID = t.NewEntity()
|
|
t.Components.Ready.Add(labelEID)
|
|
}
|
|
|
|
img, pal := layout.SpritePath, layout.PalettePath
|
|
sx, sy, base := layout.XSegments, layout.YSegments, layout.BaseFrame
|
|
|
|
spriteEID := t.SegmentedSprite(0, 0, img, pal, sx, sy, base)
|
|
|
|
entry := buttonLoadQueueEntry{
|
|
label: labelEID,
|
|
sprite: spriteEID,
|
|
}
|
|
|
|
t.buttonLoadQueue[buttonEID] = entry
|
|
|
|
return buttonEID
|
|
}
|
|
|
|
func (t *UIWidgetFactory) processButton(buttonEID akara.EID) {
|
|
// get the queue entry
|
|
entry, found := t.buttonLoadQueue[buttonEID]
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
labelEID, spriteEID := entry.label, entry.sprite
|
|
|
|
// check if the label and sprite are ready to be used
|
|
_, labelReady := t.Components.Ready.Get(labelEID)
|
|
_, spriteReady := t.Components.Ready.Get(spriteEID)
|
|
|
|
if !(labelReady && spriteReady) {
|
|
return
|
|
}
|
|
|
|
button, found := t.Components.Button.Get(buttonEID)
|
|
if !found {
|
|
button = t.Components.Button.Add(buttonEID)
|
|
}
|
|
|
|
buttonNode := t.Components.SceneGraphNode.Add(buttonEID)
|
|
|
|
sprite, found := t.Components.Sprite.Get(spriteEID)
|
|
if found {
|
|
button.Sprite = sprite.Sprite
|
|
t.Components.SceneGraphNode.Add(spriteEID).SetParent(buttonNode.Node)
|
|
}
|
|
|
|
_, found = t.Components.Label.Get(labelEID)
|
|
if found {
|
|
t.Components.SceneGraphNode.Add(labelEID).SetParent(buttonNode.Node)
|
|
}
|
|
|
|
t.processButtonStates(buttonEID)
|
|
}
|
|
|
|
func (t *UIWidgetFactory) processButtonStates(buttonEID akara.EID) {
|
|
button, _ := t.Components.Button.Get(buttonEID)
|
|
baseFrame := button.Layout.BaseFrame
|
|
|
|
hasBeenProcessed := button.States.Normal > 0
|
|
|
|
if hasBeenProcessed {
|
|
t.renderButtonStates(buttonEID)
|
|
return
|
|
}
|
|
|
|
img, pal := button.Layout.SpritePath, button.Layout.PalettePath
|
|
sx, sy := button.Layout.XSegments, button.Layout.YSegments
|
|
|
|
var normal, pressed, toggled, pressedToggled, disabled akara.EID
|
|
|
|
normal = t.SegmentedSprite(0, 0, img, pal, sx, sy, baseFrame)
|
|
|
|
// by default, all other states are whatever the normal state is
|
|
pressed = normal
|
|
toggled = normal
|
|
pressedToggled = normal
|
|
disabled = normal
|
|
|
|
button.States.Normal = normal
|
|
button.States.Pressed = pressed
|
|
button.States.Toggled = toggled
|
|
button.States.PressedToggled = pressedToggled
|
|
button.States.Disabled = disabled
|
|
|
|
// if it's got other states (most buttons do...), then we handle it
|
|
if button.Layout.HasImage && button.Layout.AllowFrameChange {
|
|
pressed = t.SegmentedSprite(0, 0, img, pal, sx, sy, baseFrame+d2button.ButtonStatePressed)
|
|
toggled = t.SegmentedSprite(0, 0, img, pal, sx, sy, baseFrame+d2button.ButtonStateToggled)
|
|
pressedToggled = t.SegmentedSprite(0, 0, img, pal, sx, sy, baseFrame+d2button.ButtonStatePressedToggled)
|
|
|
|
// also, not all buttons have a disabled state
|
|
// this stupid fucking -1 needs to be a constant
|
|
if button.Layout.DisabledFrame != isNotSegmented {
|
|
disabled = t.SegmentedSprite(0, 0, img, pal, sx, sy, button.Layout.DisabledFrame)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *UIWidgetFactory) renderButtonStates(buttonEID akara.EID) {
|
|
button, _ := t.Components.Button.Get(buttonEID)
|
|
|
|
_, rdyNormal := t.Components.Ready.Get(button.States.Normal)
|
|
_, rdyPressed := t.Components.Ready.Get(button.States.Pressed)
|
|
_, rdyToggled := t.Components.Ready.Get(button.States.Toggled)
|
|
_, rdyPressedToggled := t.Components.Ready.Get(button.States.PressedToggled)
|
|
_, rdyDisabled := t.Components.Ready.Get(button.States.Disabled)
|
|
|
|
ready := rdyNormal && rdyPressed && rdyToggled && rdyPressedToggled && rdyDisabled
|
|
|
|
if !ready {
|
|
return
|
|
}
|
|
|
|
t.Components.Ready.Add(buttonEID)
|
|
|
|
delete(t.buttonLoadQueue, buttonEID)
|
|
}
|
|
|
|
|
|
func (t *UIWidgetFactory) updateButton(buttonEID akara.EID) {
|
|
button, btnFound := t.Components.Button.Get(buttonEID)
|
|
|
|
if ! btnFound {
|
|
return
|
|
}
|
|
|
|
normal, f1 := t.Components.Texture.Get(button.States.Normal)
|
|
pressed, f2 := t.Components.Texture.Get(button.States.Pressed)
|
|
toggled, f3 := t.Components.Texture.Get(button.States.Toggled)
|
|
pressedToggled, f4 := t.Components.Texture.Get(button.States.PressedToggled)
|
|
disabled, f5 := t.Components.Texture.Get(button.States.Disabled)
|
|
|
|
if !(f1 && f2 && f3 && f4 && f5) {
|
|
return
|
|
}
|
|
|
|
button.Surfaces.Normal = normal.Texture
|
|
button.Surfaces.Pressed = pressed.Texture
|
|
button.Surfaces.Toggled = toggled.Texture
|
|
button.Surfaces.PressedToggled = pressedToggled.Texture
|
|
button.Surfaces.Disabled = disabled.Texture
|
|
|
|
texture, found := t.Components.Texture.Get(buttonEID)
|
|
if !found {
|
|
texture = t.Components.Texture.Add(buttonEID)
|
|
}
|
|
|
|
texture.Texture = button.GetCurrentTexture()
|
|
}
|