1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2025-02-04 07:37:48 -05:00
OpenDiablo2/d2core/d2systems/scene_widget_system.go
gravestench 3731e631cf
Resolve #969 (#970)
* fixed #969
2020-12-13 14:41:19 -08:00

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()
}