1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2025-02-09 10:06:35 -05:00

changes to d2components, d2systems, d2ui, d2enum

go.mod, go.sum:
* updating akara, bugfix in akara.EntityManager.RemoveEntity

d2core
* adding d2core/d2label
* adding d2core/d2bitmapfont

d2ui
* exporting some constants for use elsewhere

d2components
* added bitmap font component (for ui labels)
* added FileLoaded tag component to simplify asset loading filters
* added locale component
* FilePath component renamed to File
* sprite component now contains the sprite and palette path as strings
* adding ui label component

d2enum
* added locale as file type for file "/data/local/use"

d2systems
* changed most info prints to debug prints
* removed unused scene graph testing file (oops!)
* terminal is now rendered above mouse cursor scene
* adding ui widget system for use by the game object factory
* adding test scene for ui labels created with the ui widget system

d2systems/AppBootstrap
* added command line args for profiler
* `--testscene labels` launches the label test
* now adds the local file for processing
* game loop init logic now inside of Init method (the call to
world.Update does this)

d2systems/AssetLoader
* loads the locale file and adds a locale component that other systems
can use
* adds a FileLoaded component after finished loading a file which other
systems can use (like the loading scene)

d2systems/FileSourceResolver
* Now looks for and uses the locale for language/charset filepath
substitution

d2systems/GameClientBootstrap
* game loop init moved to end of AppBootstrap.Init

d2systems/GameObjectFactory
* embedding UI widget factory system

d2systems/BaseScene
* made base scene a little more clear by breaking the process into more
methods

d2systems/LoadingScene
* simplified the entity subscriptions by using the new FileLoaded
component

d2systems/SceneObjectFactory
* adding method for adding labels, buttons to scenes (buttons still WIP)

d2systems/SceneSpriteSystem
* the sprite system now maintains a cache of rendered sprites
This commit is contained in:
gravestench 2020-12-08 10:42:19 -08:00
parent 2e814f29b0
commit deb63a95c8
45 changed files with 1337 additions and 295 deletions

View File

@ -21,4 +21,5 @@ const (
FileTypeDT1
FileTypeWAV
FileTypeD2
FileTypeLocale
)

View File

@ -0,0 +1,128 @@
package d2bitmapfont
import (
"encoding/binary"
"image/color"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
func New(s d2interface.Sprite, table []byte, col color.Color) *BitmapFont {
return &BitmapFont{
Sprite: s,
Table: table,
Color: col,
}
}
type Glyph struct {
frame int
width int
height int
}
// BitmapFont represents a rasterized font, made from a font Table, sprite, and palette
type BitmapFont struct {
Sprite d2interface.Sprite
Table []byte
Glyphs map[rune]Glyph
Color color.Color
}
// SetColor sets the fonts Color
func (f *BitmapFont) SetColor(c color.Color) {
f.Color = c
}
// GetTextMetrics returns the dimensions of the BitmapFont element in pixels
func (f *BitmapFont) GetTextMetrics(text string) (width, height int) {
if f.Glyphs == nil {
f.initGlyphs()
}
var (
lineWidth int
lineHeight int
)
for _, c := range text {
if c == '\n' {
width = d2math.MaxInt(width, lineWidth)
height += lineHeight
lineWidth = 0
lineHeight = 0
} else if glyph, ok := f.Glyphs[c]; ok {
lineWidth += glyph.width
lineHeight = d2math.MaxInt(lineHeight, glyph.height)
}
}
width = d2math.MaxInt(width, lineWidth)
height += lineHeight
return width, height
}
// RenderText prints a text using its configured style on a Surface (multi-lines are left-aligned, use label otherwise)
func (f *BitmapFont) RenderText(text string, target d2interface.Surface) error {
if f.Glyphs == nil {
f.initGlyphs()
}
f.Sprite.SetColorMod(f.Color)
lines := strings.Split(text, "\n")
for _, line := range lines {
var (
lineHeight int
lineLength int
)
for _, c := range line {
glyph, ok := f.Glyphs[c]
if !ok {
continue
}
if err := f.Sprite.SetCurrentFrame(glyph.frame); err != nil {
return err
}
f.Sprite.Render(target)
lineHeight = d2math.MaxInt(lineHeight, glyph.height)
lineLength++
target.PushTranslation(glyph.width, 0)
}
target.PopN(lineLength)
target.PushTranslation(0, lineHeight)
}
target.PopN(len(lines))
return nil
}
func (f *BitmapFont) initGlyphs() {
_, maxCharHeight := f.Sprite.GetFrameBounds()
glyphs := make(map[rune]Glyph)
for i := 12; i < len(f.Table); i += 14 {
code := rune(binary.LittleEndian.Uint16(f.Table[i : i+2]))
var glyph Glyph
glyph.frame = int(binary.LittleEndian.Uint16(f.Table[i+8 : i+10]))
glyph.width = int(f.Table[i+3])
glyph.height = maxCharHeight
glyphs[code] = glyph
}
f.Glyphs = glyphs
}

View File

@ -0,0 +1,42 @@
package d2components
import (
"github.com/gravestench/akara"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2bitmapfont"
)
// static check that BitmapFont implements Component
var _ akara.Component = &BitmapFont{}
// BitmapFont represent a font made from a font table, a sprite, and a palette (d2 files)
type BitmapFont struct {
*d2bitmapfont.BitmapFont
}
// New creates a new BitmapFont.
func (*BitmapFont) New() akara.Component {
return &BitmapFont{}
}
// BitmapFontFactory is a wrapper for the generic component factory that returns BitmapFont component instances.
// This can be embedded inside of a system to give them the methods for adding, retrieving, and removing a BitmapFont.
type BitmapFontFactory struct {
BitmapFont *akara.ComponentFactory
}
// AddBitmapFont adds a BitmapFont component to the given entity and returns it
func (m *BitmapFontFactory) AddBitmapFont(id akara.EID) *BitmapFont {
return m.BitmapFont.Add(id).(*BitmapFont)
}
// GetBitmapFont returns the BitmapFont component for the given entity, and a bool for whether or not it exists
func (m *BitmapFontFactory) GetBitmapFont(id akara.EID) (*BitmapFont, bool) {
component, found := m.BitmapFont.Get(id)
if !found {
return nil, found
}
return component.(*BitmapFont), found
}

View File

@ -0,0 +1,38 @@
//nolint:dupl,golint,stylecheck // component declarations are supposed to look the same
package d2components
import (
"github.com/gravestench/akara"
)
// static check that FileLoaded implements Component
var _ akara.Component = &FileLoaded{}
// FileLoaded is used to flag file entities as having been loaded. it is an empty struct.
type FileLoaded struct {}
// New returns a FileLoaded component. By default, it contains an empty string.
func (*FileLoaded) New() akara.Component {
return &FileLoaded{}
}
// FileLoadedFactory is a wrapper for the generic component factory that returns FileLoaded component instances.
// This can be embedded inside of a system to give them the methods for adding, retrieving, and removing a FileLoaded.
type FileLoadedFactory struct {
FileLoaded *akara.ComponentFactory
}
// AddFileLoaded adds a FileLoaded component to the given entity and returns it
func (m *FileLoadedFactory) AddFileLoaded(id akara.EID) *FileLoaded {
return m.FileLoaded.Add(id).(*FileLoaded)
}
// GetFileLoaded returns the FileLoaded component for the given entity, and a bool for whether or not it exists
func (m *FileLoadedFactory) GetFileLoaded(id akara.EID) (*FileLoaded, bool) {
component, found := m.FileLoaded.Get(id)
if !found {
return nil, found
}
return component.(*FileLoaded), found
}

View File

@ -5,36 +5,36 @@ import (
"github.com/gravestench/akara"
)
// static check that FilePath implements Component
var _ akara.Component = &FilePath{}
// static check that File implements Component
var _ akara.Component = &File{}
// FilePath represents a file path for a file
type FilePath struct {
// File represents a file as a path
type File struct {
Path string
}
// New returns a FilePath component. By default, it contains an empty string.
func (*FilePath) New() akara.Component {
return &FilePath{}
// New returns a File component. By default, it contains an empty string.
func (*File) New() akara.Component {
return &File{}
}
// FilePathFactory is a wrapper for the generic component factory that returns FilePath component instances.
// This can be embedded inside of a system to give them the methods for adding, retrieving, and removing a FilePath.
type FilePathFactory struct {
FilePath *akara.ComponentFactory
// FileFactory is a wrapper for the generic component factory that returns File component instances.
// This can be embedded inside of a system to give them the methods for adding, retrieving, and removing a File.
type FileFactory struct {
File *akara.ComponentFactory
}
// AddFilePath adds a FilePath component to the given entity and returns it
func (m *FilePathFactory) AddFilePath(id akara.EID) *FilePath {
return m.FilePath.Add(id).(*FilePath)
// AddFile adds a File component to the given entity and returns it
func (m *FileFactory) AddFile(id akara.EID) *File {
return m.File.Add(id).(*File)
}
// GetFilePath returns the FilePath component for the given entity, and a bool for whether or not it exists
func (m *FilePathFactory) GetFilePath(id akara.EID) (*FilePath, bool) {
component, found := m.FilePath.Get(id)
// GetFile returns the File component for the given entity, and a bool for whether or not it exists
func (m *FileFactory) GetFile(id akara.EID) (*File, bool) {
component, found := m.File.Get(id)
if !found {
return nil, found
}
return component.(*FilePath), found
return component.(*File), found
}

View File

@ -13,7 +13,7 @@ var _ akara.Component = &FileSource{}
// AbstractSource is the abstract representation of what a file source is
type AbstractSource interface {
Path() string // the path of the source itself
Open(path *FilePath) (d2interface.DataStream, error)
Open(path *File) (d2interface.DataStream, error)
}
// FileSource contains an embedded file source interface, something that can open files

View File

@ -0,0 +1,41 @@
//nolint:dupl,golint,stylecheck // component declarations are supposed to look the same
package d2components
import (
"github.com/gravestench/akara"
)
// static check that Locale implements Component
var _ akara.Component = &Locale{}
// Locale represents a file as a path
type Locale struct {
Code byte
String string
}
// New returns a Locale component. By default, it contains an empty string.
func (*Locale) New() akara.Component {
return &Locale{}
}
// LocaleFactory is a wrapper for the generic component factory that returns Locale component instances.
// This can be embedded inside of a system to give them the methods for adding, retrieving, and removing a Locale.
type LocaleFactory struct {
Locale *akara.ComponentFactory
}
// AddLocale adds a Locale component to the given entity and returns it
func (m *LocaleFactory) AddLocale(id akara.EID) *Locale {
return m.Locale.Add(id).(*Locale)
}
// GetLocale returns the Locale component for the given entity, and a bool for whether or not it exists
func (m *LocaleFactory) GetLocale(id akara.EID) (*Locale, bool) {
component, found := m.Locale.Get(id)
if !found {
return nil, found
}
return component.(*Locale), found
}

View File

@ -13,6 +13,7 @@ var _ akara.Component = &Sprite{}
// Sprite is a component that contains a width and height
type Sprite struct {
d2interface.Sprite
SpritePath, PalettePath string
}
// New returns an animation component. By default, it contains a nil instance of an animation.

View File

@ -0,0 +1,43 @@
//nolint:dupl,golint,stylecheck // component declarations are supposed to look the same
package d2components
import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2label"
"github.com/gravestench/akara"
)
// static check that Label implements Component
var _ akara.Component = &Label{}
// Label represents a ui label. It contains an embedded *d2label.Label
type Label struct {
*d2label.Label
}
// New returns a Label component. By default, it contains an empty string.
func (*Label) New() akara.Component {
return &Label{
d2label.New(),
}
}
// LabelFactory is a wrapper for the generic component factory that returns Label component instances.
// This can be embedded inside of a system to give them the methods for adding, retrieving, and removing a Label.
type LabelFactory struct {
Label *akara.ComponentFactory
}
// AddLabel adds a Label component to the given entity and returns it
func (m *LabelFactory) AddLabel(id akara.EID) *Label {
return m.Label.Add(id).(*Label)
}
// GetLabel returns the Label component for the given entity, and a bool for whether or not it exists
func (m *LabelFactory) GetLabel(id akara.EID) (*Label, bool) {
component, found := m.Label.Get(id)
if !found {
return nil, found
}
return component.(*Label), found
}

184
d2core/d2label/label.go Normal file
View File

@ -0,0 +1,184 @@
package d2label
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"image/color"
"regexp"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2bitmapfont"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
// New creates a new label, initializing the unexported fields
func New() *Label {
return &Label{
colors: map[int]color.Color{0: color.White},
}
}
// Label represents a user interface label
type Label struct {
dirty bool // used to flag when to re-render the label
text string
textWithColorTokens string
Alignment d2ui.HorizontalAlign
Font *d2bitmapfont.BitmapFont
colors map[int]color.Color
backgroundColor color.Color
}
func (v *Label) Render(target d2interface.Surface) {
lines := strings.Split(v.GetText(), "\n")
yOffset := 0
lastColor := v.colors[0]
v.Font.SetColor(lastColor)
for _, line := range lines {
lw, lh := v.GetTextMetrics(line)
characters := []rune(line)
if v.backgroundColor != nil {
target.Clear(v.backgroundColor)
}
target.PushTranslation(v.GetAlignOffset(lw), yOffset)
for idx := range characters {
character := string(characters[idx])
charWidth, _ := v.GetTextMetrics(character)
if v.colors[idx] != nil {
lastColor = v.colors[idx]
v.Font.SetColor(lastColor)
}
_ = v.Font.RenderText(character, target)
target.PushTranslation(charWidth, 0)
}
target.PopN(len(characters))
yOffset += lh
target.Pop()
}
v.dirty = false
}
// IsDirty returns if the label needs to be re-rendered
func (v *Label) IsDirty() bool {
return v.dirty
}
// GetSize returns the size of the label
func (v *Label) GetSize() (width, height int) {
return v.Font.GetTextMetrics(v.text)
}
// GetTextMetrics returns the width and height of the enclosing rectangle in Pixels.
func (v *Label) GetTextMetrics(text string) (width, height int) {
return v.Font.GetTextMetrics(text)
}
// SetText sets the label's text
func (v *Label) SetText(newText string) {
if v.text == newText {
return
}
v.text = newText
v.dirty = true
v.textWithColorTokens = v.processColorTokens(v.text)
}
// GetText returns the label's textWithColorTokens
func (v *Label) GetText() string {
return v.text
}
// SetBackgroundColor sets the background highlight color
func (v *Label) SetBackgroundColor(c color.Color) {
v.dirty = true
v.backgroundColor = c
}
func (v *Label) processColorTokens(str string) string {
tokenMatch := regexp.MustCompile(d2ui.ColorTokenMatch)
tokenStrMatch := regexp.MustCompile(d2ui.ColorStrMatch)
empty := []byte("")
tokenPosition := 0
withoutTokens := string(tokenMatch.ReplaceAll([]byte(str), empty)) // remove tokens from string
matches := tokenStrMatch.FindAll([]byte(str), -1)
if len(matches) == 0 {
v.colors[0] = getColor(d2ui.ColorTokenWhite)
}
// we find the index of each token and update the color map.
// the key in the map is the starting index of each color token, the value is the color
for idx := range matches {
match := matches[idx]
matchToken := tokenMatch.Find(match)
matchStr := string(tokenMatch.ReplaceAll(match, empty))
token := d2ui.ColorToken(matchToken)
theColor := getColor(token)
if theColor == nil {
continue
}
if v.colors == nil {
v.colors = make(map[int]color.Color)
}
v.colors[tokenPosition] = theColor
tokenPosition += len(matchStr)
}
return withoutTokens
}
func (v *Label) GetAlignOffset(textWidth int) int {
switch v.Alignment {
case d2ui.HorizontalAlignLeft:
return 0
case d2ui.HorizontalAlignCenter:
return -textWidth / 2
case d2ui.HorizontalAlignRight:
return -textWidth
default:
return 0
}
}
func getColor(token d2ui.ColorToken) color.Color {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/823
colors := map[d2ui.ColorToken]color.Color{
d2ui.ColorTokenGrey: d2util.Color(d2ui.ColorGrey100Alpha),
d2ui.ColorTokenWhite: d2util.Color(d2ui.ColorWhite100Alpha),
d2ui.ColorTokenBlue: d2util.Color(d2ui.ColorBlue100Alpha),
d2ui.ColorTokenYellow: d2util.Color(d2ui.ColorYellow100Alpha),
d2ui.ColorTokenGreen: d2util.Color(d2ui.ColorGreen100Alpha),
d2ui.ColorTokenGold: d2util.Color(d2ui.ColorGold100Alpha),
d2ui.ColorTokenOrange: d2util.Color(d2ui.ColorOrange100Alpha),
d2ui.ColorTokenRed: d2util.Color(d2ui.ColorRed100Alpha),
d2ui.ColorTokenBlack: d2util.Color(d2ui.ColorBlack100Alpha),
}
chosen := colors[token]
if chosen == nil {
return nil
}
return chosen
}

View File

@ -2,9 +2,12 @@ package d2systems
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/pkg/profile"
"gopkg.in/alecthomas/kingpin.v2"
"os"
"path"
"strings"
"github.com/gravestench/akara"
@ -33,6 +36,9 @@ const (
skipSplashArg = "nosplash"
skipSplashDesc = "skip the ebiten splash screen"
profilerArg = "profile"
profilerDesc = "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)"
)
// static check that the game config system implements the system interface
@ -46,7 +52,7 @@ type AppBootstrap struct {
subscribedFiles *akara.Subscription
subscribedConfigs *akara.Subscription
d2components.GameConfigFactory
d2components.FilePathFactory
d2components.FileFactory
d2components.FileTypeFactory
d2components.FileHandleFactory
d2components.FileSourceFactory
@ -58,16 +64,21 @@ func (m *AppBootstrap) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupSubscriptions()
m.setupFactories()
m.injectSystems()
m.setupConfigSources()
m.setupConfigFile()
m.setupLocaleFile()
m.parseCommandLineArgs()
m.Info("... initialization complete!")
m.Debug("... initialization complete!")
if err := m.World.Update(0); err != nil {
m.Error(err.Error())
}
}
func (m *AppBootstrap) setupLogger() {
@ -76,14 +87,14 @@ func (m *AppBootstrap) setupLogger() {
}
func (m *AppBootstrap) setupSubscriptions() {
m.Info("setting up component subscriptions")
m.Debug("setting up component subscriptions")
// we are going to check entities that dont yet have loaded asset types
filesToCheck := m.NewComponentFilter().
Require( // files that need to be loaded
&d2components.FileType{},
&d2components.FileHandle{},
&d2components.FilePath{},
&d2components.File{},
).
Forbid( // files which have been loaded
&d2components.GameConfig{},
@ -109,10 +120,10 @@ func (m *AppBootstrap) setupSubscriptions() {
}
func (m *AppBootstrap) setupFactories() {
m.Info("setting up component factories")
m.Debug("setting up component factories")
m.InjectComponent(&d2components.GameConfig{}, &m.GameConfig)
m.InjectComponent(&d2components.FilePath{}, &m.FilePath)
m.InjectComponent(&d2components.File{}, &m.File)
m.InjectComponent(&d2components.FileType{}, &m.FileType)
m.InjectComponent(&d2components.FileHandle{}, &m.FileHandle)
m.InjectComponent(&d2components.FileSource{}, &m.FileSource)
@ -145,7 +156,7 @@ func (m *AppBootstrap) setupConfigSources() {
e1, e2 := m.NewEntity(), m.NewEntity()
// add file path components to these entities
fp1, fp2 := m.AddFilePath(e1), m.AddFilePath(e2)
fp1, fp2 := m.AddFile(e1), m.AddFile(e2)
// the first entity gets a filepath for the od2 directory, this one is checked first
// eg. if OD2 binary is in `~/src/OpenDiablo2/`, then this directory is checked first for a config file
@ -168,10 +179,16 @@ func (m *AppBootstrap) setupConfigSources() {
func (m *AppBootstrap) setupConfigFile() {
// add an entity that will get picked up by the game config system and loaded
m.AddFilePath(m.NewEntity()).Path = configFileName
m.AddFile(m.NewEntity()).Path = configFileName
m.Infof("setting up config file `%s` for processing", configFileName)
}
func (m *AppBootstrap) setupLocaleFile() {
// add an entity that will get picked up by the game config system and loaded
m.AddFile(m.NewEntity()).Path = d2resource.LocalLanguage
m.Infof("setting up locale file `%s` for processing", d2resource.LocalLanguage)
}
// Update will look for the first entity with a game config component
// and then add the mpq's as file sources
func (m *AppBootstrap) Update() {
@ -180,7 +197,7 @@ func (m *AppBootstrap) Update() {
return
}
m.Infof("found %d new configs to parse", len(configs))
m.Debugf("found %d new configs to parse", len(configs))
firstConfigEntityID := configs[0]
@ -203,12 +220,13 @@ func (m *AppBootstrap) initMpqSources(cfg *d2components.GameConfig) {
m.Infof("adding mpq: %s", fullMpqFilePath)
// make a new entity for the mpq file source
mpqSource := m.AddFilePath(m.NewEntity())
mpqSource := m.AddFile(m.NewEntity())
mpqSource.Path = fullMpqFilePath
}
}
func (m *AppBootstrap) parseCommandLineArgs() {
profilerOptions := kingpin.Flag(profilerArg, profilerDesc).String()
sceneTest := kingpin.Flag(sceneTestArg, sceneTestDesc).String()
server := kingpin.Flag(serverArg, serverDesc).Bool()
enableCounter := kingpin.Flag(counterArg, counterDesc).Bool()
@ -216,6 +234,8 @@ func (m *AppBootstrap) parseCommandLineArgs() {
kingpin.Parse()
m.parseProfilerOptions(*profilerOptions)
if *enableCounter {
m.World.AddSystem(&UpdateCounter{})
}
@ -227,6 +247,7 @@ func (m *AppBootstrap) parseCommandLineArgs() {
m.World.AddSystem(&RenderSystem{})
m.World.AddSystem(&InputSystem{})
m.World.AddSystem(&GameObjectFactory{})
switch *sceneTest {
case "splash":
@ -244,7 +265,53 @@ func (m *AppBootstrap) parseCommandLineArgs() {
case "terminal":
m.Info("running terminal scene")
m.World.AddSystem(NewTerminalScene())
case "labels":
m.Info("running label test scene")
m.World.AddSystem(NewLabelTestScene())
default:
m.World.AddSystem(&GameClientBootstrap{})
}
}
func (m *AppBootstrap) parseProfilerOptions(profileOption string) interface{ Stop() } {
var options []func(*profile.Profile)
switch strings.ToLower(strings.Trim(profileOption, " ")) {
case "cpu":
m.Debug("CPU profiling is enabled.")
options = append(options, profile.CPUProfile)
case "mem":
m.Debug("Memory profiling is enabled.")
options = append(options, profile.MemProfile)
case "block":
m.Debug("Block profiling is enabled.")
options = append(options, profile.BlockProfile)
case "goroutine":
m.Debug("Goroutine profiling is enabled.")
options = append(options, profile.GoroutineProfile)
case "trace":
m.Debug("Trace profiling is enabled.")
options = append(options, profile.TraceProfile)
case "thread":
m.Debug("Thread creation profiling is enabled.")
options = append(options, profile.ThreadcreationProfile)
case "mutex":
m.Debug("Mutex profiling is enabled.")
options = append(options, profile.MutexProfile)
}
options = append(options, profile.ProfilePath("./pprof/"))
if len(options) > 1 {
return profile.Start(options...)
}
return nil
}

View File

@ -1,6 +1,7 @@
package d2systems
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"io"
"github.com/gravestench/akara"
@ -40,7 +41,8 @@ type AssetLoaderSystem struct {
fileSub *akara.Subscription
sourceSub *akara.Subscription
cache *d2cache.Cache
d2components.FilePathFactory
localeString string // related to file "/data/local/use"
d2components.FileFactory
d2components.FileTypeFactory
d2components.FileHandleFactory
d2components.FileSourceFactory
@ -56,6 +58,9 @@ type AssetLoaderSystem struct {
d2components.Dt1Factory
d2components.WavFactory
d2components.AnimationDataFactory
d2components.LocaleFactory
d2components.BitmapFontFactory
d2components.FileLoadedFactory
}
// Init injects component maps related to various asset types
@ -64,7 +69,7 @@ func (m *AssetLoaderSystem) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupSubscriptions()
m.setupFactories()
@ -78,29 +83,18 @@ func (m *AssetLoaderSystem) setupLogger() {
}
func (m *AssetLoaderSystem) setupSubscriptions() {
m.Info("setting up component subscriptions")
m.Debug("setting up component subscriptions")
// we are going to check entities that dont yet have loaded asset types
filesToLoad := m.NewComponentFilter().
Require(
&d2components.FilePath{}, // we want to process entities with these file components
&d2components.File{}, // we want to process entities with these file components
&d2components.FileType{},
&d2components.FileHandle{},
).
Forbid(
&d2components.FileSource{}, // but we forbid files that are already loaded
&d2components.GameConfig{},
&d2components.StringTable{},
&d2components.DataDictionary{},
&d2components.Palette{},
&d2components.PaletteTransform{},
&d2components.Cof{},
&d2components.Dc6{},
&d2components.Dcc{},
&d2components.Ds1{},
&d2components.Dt1{},
&d2components.Wav{},
&d2components.AnimationData{},
&d2components.FileSource{},
&d2components.FileLoaded{},
).
Build()
@ -113,9 +107,9 @@ func (m *AssetLoaderSystem) setupSubscriptions() {
}
func (m *AssetLoaderSystem) setupFactories() {
m.Info("setting up component factories")
m.Debug("setting up component factories")
m.InjectComponent(&d2components.FilePath{}, &m.FilePath)
m.InjectComponent(&d2components.File{}, &m.File)
m.InjectComponent(&d2components.FileType{}, &m.FileType)
m.InjectComponent(&d2components.FileHandle{}, &m.FileHandle)
m.InjectComponent(&d2components.FileSource{}, &m.FileSource)
@ -131,6 +125,9 @@ func (m *AssetLoaderSystem) setupFactories() {
m.InjectComponent(&d2components.Dt1{}, &m.Dt1)
m.InjectComponent(&d2components.Wav{}, &m.Wav)
m.InjectComponent(&d2components.AnimationData{}, &m.AnimationData)
m.InjectComponent(&d2components.Locale{}, &m.Locale)
m.InjectComponent(&d2components.BitmapFont{}, &m.BitmapFont)
m.InjectComponent(&d2components.FileLoaded{}, &m.FileLoaded)
}
// Update processes all of the Entities in the subscription of file entities that need to be processed
@ -142,7 +139,7 @@ func (m *AssetLoaderSystem) Update() {
func (m *AssetLoaderSystem) loadAsset(id akara.EID) {
// make sure everything is kosher
fp, found := m.GetFilePath(id)
fp, found := m.GetFile(id)
if !found {
m.Errorf("filepath component not found for entity %d", id)
return
@ -166,6 +163,8 @@ func (m *AssetLoaderSystem) loadAsset(id akara.EID) {
return
}
m.Debugf("Loading file: %s", fp.Path)
// make sure to seek back to 0 if the filehandle was cached
_, _ = fh.Data.Seek(0, 0)
@ -218,6 +217,8 @@ func (m *AssetLoaderSystem) assignFromCache(id akara.EID, path string, t d2enum.
m.AddAnimationData(id).AnimationData = entry.(*d2animdata.AnimationData)
}
m.AddFileLoaded(id)
return found
}
@ -225,58 +226,58 @@ func (m *AssetLoaderSystem) assignFromCache(id akara.EID, path string, t d2enum.
func (m *AssetLoaderSystem) parseAndCache(id akara.EID, path string, t d2enum.FileType, data []byte) {
switch t {
case d2enum.FileTypeStringTable:
m.Infof("Loading string table: %s", path)
m.Debugf("Loading string table: %s", path)
m.loadStringTable(id, path, data)
case d2enum.FileTypeFontTable:
m.Infof("Loading font table: %s", path)
m.Debugf("Loading font table: %s", path)
m.loadFontTable(id, path, data)
case d2enum.FileTypeDataDictionary:
m.Infof("Loading data dictionary: %s", path)
m.Debugf("Loading data dictionary: %s", path)
m.loadDataDictionary(id, path, data)
case d2enum.FileTypePalette:
m.Infof("Loading palette: %s", path)
m.Debugf("Loading palette: %s", path)
if err := m.loadPalette(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypePaletteTransform:
m.Infof("Loading palette transform: %s", path)
m.Debugf("Loading palette transform: %s", path)
if err := m.loadPaletteTransform(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypeCOF:
m.Infof("Loading COF: %s", path)
m.Debugf("Loading COF: %s", path)
if err := m.loadCOF(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypeDC6:
m.Infof("Loading DC6: %s", path)
m.Debugf("Loading DC6: %s", path)
if err := m.loadDC6(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypeDCC:
m.Infof("Loading DCC: %s", path)
m.Debugf("Loading DCC: %s", path)
if err := m.loadDCC(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypeDS1:
m.Infof("Loading DS1: %s", path)
m.Debugf("Loading DS1: %s", path)
if err := m.loadDS1(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypeDT1:
m.Infof("Loading DT1: %s", path)
m.Debugf("Loading DT1: %s", path)
if err := m.loadDT1(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypeWAV:
m.Infof("Loading WAV: %s", path)
m.Debugf("Loading WAV: %s", path)
fh, found := m.GetFileHandle(id)
if !found {
@ -285,12 +286,27 @@ func (m *AssetLoaderSystem) parseAndCache(id akara.EID, path string, t d2enum.Fi
m.loadWAV(id, path, fh.Data)
case d2enum.FileTypeD2:
m.Infof("Loading animation data: %s", path)
m.Debugf("Loading animation data: %s", path)
if err := m.loadAnimationData(id, path, data); err != nil {
m.Error(err.Error())
}
case d2enum.FileTypeLocale:
m.Debugf("Loading locale: %s", path)
m.loadLocale(id, data)
}
m.AddFileLoaded(id)
}
func (m *AssetLoaderSystem) loadLocale(id akara.EID, data []byte) {
locale := m.AddLocale(id)
locale.Code = data[0]
locale.String = d2resource.GetLanguageLiteral(locale.Code)
m.localeString = locale.String
}
func (m *AssetLoaderSystem) loadStringTable(id akara.EID, path string, data []byte) {

View File

@ -1,6 +1,7 @@
package d2systems
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
@ -35,7 +36,7 @@ const (
//
// A file source can be something like an MPQ archive or a file system directory on the host machine.
//
// A file handle is a primitive representation of a loaded file; something that has data
// A file handle is a primitive representation of a loaded file; something that has data
// in the form of a byte slice, but has not been parsed into a more meaningful struct, like a DC6 animation.
type FileHandleResolver struct {
akara.BaseSubscriberSystem
@ -43,10 +44,16 @@ type FileHandleResolver struct {
cache *d2cache.Cache
filesToLoad *akara.Subscription
sourcesToUse *akara.Subscription
d2components.FilePathFactory
localesToCheck *akara.Subscription
locale struct {
charset string
language string
}
d2components.FileFactory
d2components.FileTypeFactory
d2components.FileSourceFactory
d2components.FileHandleFactory
d2components.LocaleFactory
}
// Init initializes the system with the given world
@ -57,12 +64,12 @@ func (m *FileHandleResolver) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupSubscriptions()
m.setupFactories()
m.Info("... initialization complete!")
m.Debug("... initialization complete!")
}
func (m *FileHandleResolver) setupLogger() {
@ -71,11 +78,11 @@ func (m *FileHandleResolver) setupLogger() {
}
func (m *FileHandleResolver) setupSubscriptions() {
m.Info("setting up component subscriptions")
m.Debug("setting up component subscriptions")
// this filter is for entities that have a file path and file type but no file handle.
filesToLoad := m.NewComponentFilter().
Require(
&d2components.FilePath{},
&d2components.File{},
&d2components.FileType{},
).
Forbid(
@ -88,17 +95,23 @@ func (m *FileHandleResolver) setupSubscriptions() {
Require(&d2components.FileSource{}).
Build()
localesToCheck := m.NewComponentFilter().
Require(&d2components.Locale{}).
Build()
m.filesToLoad = m.AddSubscription(filesToLoad)
m.sourcesToUse = m.AddSubscription(sourcesToUse)
m.localesToCheck = m.AddSubscription(localesToCheck)
}
func (m *FileHandleResolver) setupFactories() {
m.Info("setting up component factories")
m.Debug("setting up component factories")
m.InjectComponent(&d2components.FilePath{}, &m.FilePath)
m.InjectComponent(&d2components.File{}, &m.File)
m.InjectComponent(&d2components.FileType{}, &m.FileType)
m.InjectComponent(&d2components.FileHandle{}, &m.FileHandle)
m.InjectComponent(&d2components.FileSource{}, &m.FileSource)
m.InjectComponent(&d2components.Locale{}, &m.Locale)
}
// Update iterates over entities which have not had a file handle resolved.
@ -107,6 +120,20 @@ func (m *FileHandleResolver) setupFactories() {
func (m *FileHandleResolver) Update() {
filesToLoad := m.filesToLoad.GetEntities()
sourcesToUse := m.sourcesToUse.GetEntities()
locales := m.localesToCheck.GetEntities()
if m.locale.charset == "" && m.locale.language == "" {
for _, eid := range locales {
locale, _ := m.GetLocale(eid)
m.locale.language = locale.String
m.locale.charset = d2resource.GetFontCharset(locale.String)
m.RemoveEntity(eid)
}
if m.locale.charset != "" && m.locale.language != "" {
m.Infof("locale set to `%s`", m.locale.language)
}
}
for _, fileID := range filesToLoad {
for _, sourceID := range sourcesToUse {
@ -119,7 +146,7 @@ func (m *FileHandleResolver) Update() {
// try to load a file with a source, returns true if loaded
func (m *FileHandleResolver) loadFileWithSource(fileID, sourceID akara.EID) bool {
fp, found := m.GetFilePath(fileID)
fp, found := m.GetFile(fileID)
if !found {
return false
}
@ -134,16 +161,16 @@ func (m *FileHandleResolver) loadFileWithSource(fileID, sourceID akara.EID) bool
return false
}
sourceFp, found := m.GetFilePath(sourceID)
sourceFp, found := m.GetFile(sourceID)
if !found {
return false
}
// replace the locale tokens if present
if strings.Contains(fp.Path, languageTokenFont) {
fp.Path = strings.ReplaceAll(fp.Path, languageTokenFont, "latin")
} else if strings.Contains(fp.Path, languageTokenStringTable) {
fp.Path = strings.ReplaceAll(fp.Path, languageTokenStringTable, "ENG")
if strings.Contains(fp.Path, languageTokenFont) && m.locale.charset != "" {
fp.Path = strings.ReplaceAll(fp.Path, d2resource.LanguageFontToken, m.locale.charset)
} else if strings.Contains(fp.Path, languageTokenStringTable) && m.locale.language != "" {
fp.Path = strings.ReplaceAll(fp.Path, d2resource.LanguageTableToken, m.locale.language)
}
cacheKey := m.makeCacheKey(fp.Path, sourceFp.Path)
@ -167,7 +194,7 @@ func (m *FileHandleResolver) loadFileWithSource(fileID, sourceID akara.EID) bool
}
tryPath := strings.ReplaceAll(fp.Path, "sfx", "music")
tmpComponent := &d2components.FilePath{Path: tryPath}
tmpComponent := &d2components.File{Path: tryPath}
cacheKey = m.makeCacheKey(tryPath, sourceFp.Path)
if entry, found := m.cache.Retrieve(cacheKey); found {
@ -186,7 +213,7 @@ func (m *FileHandleResolver) loadFileWithSource(fileID, sourceID akara.EID) bool
fp.Path = tryPath
}
m.Infof("resolved `%s` with source `%s`", fp.Path, sourceFp.Path)
m.Debugf("resolved `%s` with source `%s`", fp.Path, sourceFp.Path)
component := m.AddFileHandle(fileID)
component.Data = data

View File

@ -24,15 +24,15 @@ func Test_FileHandleResolver_Process(t *testing.T) {
world := akara.NewWorld(cfg)
filePaths := typeSys.FilePathFactory
filePaths := typeSys.FileFactory
fileHandles := handleSys.FileHandleFactory
sourceEntity := world.NewEntity()
source := filePaths.AddFilePath(sourceEntity)
source := filePaths.AddFile(sourceEntity)
source.Path = testDataPath
fileEntity := world.NewEntity()
file := filePaths.AddFilePath(fileEntity)
file := filePaths.AddFile(fileEntity)
file.Path = "testfile_a.txt"
_ = world.Update(0)

View File

@ -25,8 +25,8 @@ const (
type FileSourceResolver struct {
akara.BaseSubscriberSystem
*d2util.Logger
filesToCheck *akara.Subscription
d2components.FilePathFactory
filesToCheck *akara.Subscription
d2components.FileFactory
d2components.FileTypeFactory
d2components.FileSourceFactory
}
@ -37,12 +37,12 @@ func (m *FileSourceResolver) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupSubscriptions()
m.setupFactories()
m.Info("... initialization complete!")
m.Debug("... initialization complete!")
}
func (m *FileSourceResolver) setupLogger() {
@ -51,12 +51,12 @@ func (m *FileSourceResolver) setupLogger() {
}
func (m *FileSourceResolver) setupSubscriptions() {
m.Info("setting up component subscriptions")
m.Debug("setting up component subscriptions")
// subscribe to entities with a file type and file path, but no file source type
filesToCheck := m.NewComponentFilter().
Require(
&d2components.FilePath{},
&d2components.File{},
&d2components.FileType{},
).
Forbid(
@ -68,24 +68,22 @@ func (m *FileSourceResolver) setupSubscriptions() {
}
func (m *FileSourceResolver) setupFactories() {
m.Info("setting up component factories")
m.Debug("setting up component factories")
m.InjectComponent(&d2components.FilePath{}, &m.FilePath)
m.InjectComponent(&d2components.File{}, &m.File)
m.InjectComponent(&d2components.FileType{}, &m.FileType)
m.InjectComponent(&d2components.FileSource{}, &m.FileSource)
}
// Update iterates over entities from its subscription, and checks if it can be used as a file source
func (m *FileSourceResolver) Update() {
for subIdx := range m.Subscriptions {
for _, sourceEntityID := range m.Subscriptions[subIdx].GetEntities() {
m.processSourceEntity(sourceEntityID)
}
for _, eid := range m.filesToCheck.GetEntities() {
m.processSourceEntity(eid)
}
}
func (m *FileSourceResolver) processSourceEntity(id akara.EID) {
fp, found := m.GetFilePath(id)
fp, found := m.GetFile(id)
if !found {
return
}
@ -109,10 +107,10 @@ func (m *FileSourceResolver) processSourceEntity(id akara.EID) {
m.AddFileSource(id).AbstractSource = instance
m.Infof("using MPQ source for `%s`", fp.Path)
m.Debugf("adding MPQ source: `%s`", fp.Path)
case d2enum.FileTypeDirectory:
m.AddFileSource(id).AbstractSource = m.makeFileSystemSource(fp.Path)
m.Infof("using FILESYSTEM source for `%s`", fp.Path)
m.Debugf("adding FILESYSTEM source: `%s`", fp.Path)
}
}
@ -125,7 +123,7 @@ type fsSource struct {
rootDir string
}
func (s *fsSource) Open(path *d2components.FilePath) (d2interface.DataStream, error) {
func (s *fsSource) Open(path *d2components.File) (d2interface.DataStream, error) {
fileData, err := os.Open(s.fullPath(path.Path))
if err != nil {
return nil, err
@ -156,7 +154,7 @@ type mpqSource struct {
mpq d2interface.Archive
}
func (s *mpqSource) Open(path *d2components.FilePath) (d2interface.DataStream, error) {
func (s *mpqSource) Open(path *d2components.File) (d2interface.DataStream, error) {
fileData, err := s.mpq.ReadFileStream(s.cleanMpqPath(path.Path))
if err != nil {
return nil, err

View File

@ -21,11 +21,11 @@ func Test_FileSourceResolution(t *testing.T) {
world := akara.NewWorld(cfg)
filePaths := typeSys.FilePathFactory
filePaths := typeSys.FileFactory
fileSources := sourceSys.FileSourceFactory
sourceEntity := world.NewEntity()
sourceFp := filePaths.AddFilePath(sourceEntity)
sourceFp := filePaths.AddFile(sourceEntity)
sourceFp.Path = testDataPath
_ = world.Update(0)

View File

@ -1,6 +1,7 @@
package d2systems
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"os"
"path/filepath"
"strings"
@ -30,7 +31,7 @@ type FileTypeResolver struct {
akara.BaseSubscriberSystem
*d2util.Logger
filesToCheck *akara.Subscription
d2components.FilePathFactory
d2components.FileFactory
d2components.FileTypeFactory
}
@ -40,7 +41,7 @@ func (m *FileTypeResolver) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupFactories()
m.setupSubscriptions()
@ -52,7 +53,7 @@ func (m *FileTypeResolver) setupLogger() {
}
func (m *FileTypeResolver) setupFactories() {
m.InjectComponent(&d2components.FilePath{}, &m.FilePath)
m.InjectComponent(&d2components.File{}, &m.File)
m.InjectComponent(&d2components.FileType{}, &m.FileType)
}
@ -60,7 +61,7 @@ func (m *FileTypeResolver) setupSubscriptions() {
// we subscribe only to entities that have a filepath
// and have not yet been given a file type
filesToCheck := m.NewComponentFilter().
Require(&d2components.FilePath{}).
Require(&d2components.File{}).
Forbid(&d2components.FileType{}).
Build()
@ -76,17 +77,25 @@ func (m *FileTypeResolver) Update() {
//nolint:gocyclo // this big switch statement is unfortunate, but necessary
func (m *FileTypeResolver) determineFileType(id akara.EID) {
fp, found := m.GetFilePath(id)
fp, found := m.GetFile(id)
if !found {
return
}
ft := m.AddFileType(id)
// try to immediately load as an mpq
if _, err := d2mpq.Load(fp.Path); err == nil {
ft.Type = d2enum.FileTypeMPQ
return
}
// special case for the locale file
if fp.Path == d2resource.LocalLanguage {
ft.Type = d2enum.FileTypeLocale
return
}
ext := strings.ToLower(filepath.Ext(fp.Path))
switch ext {

View File

@ -13,7 +13,7 @@ func TestNewFileTypeResolver_KnownType(t *testing.T) {
world := akara.NewWorld(akara.NewWorldConfig().With(typeSys))
e := world.NewEntity()
typeSys.AddFilePath(e).Path = "/some/path/to/a/file.dcc"
typeSys.AddFile(e).Path = "/some/path/to/a/file.dcc"
if len(typeSys.filesToCheck.GetEntities()) != 1 {
t.Error("entity with file path not added to file type typeSys subscription")
@ -41,7 +41,7 @@ func TestNewFileTypeResolver_UnknownType(t *testing.T) {
e := world.NewEntity()
fp := typeSys.AddFilePath(e)
fp := typeSys.AddFile(e)
fp.Path = "/some/path/to/a/file.XYZ"
_ = world.Update(0)

View File

@ -26,15 +26,11 @@ func (m *GameClientBootstrap) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.injectSystems()
m.Info("initialization complete")
if err := m.World.Update(0); err != nil {
m.Error(err.Error())
}
m.Debug("initialization complete")
}
func (m *GameClientBootstrap) setupLogger() {
@ -72,6 +68,6 @@ func (m *GameClientBootstrap) injectSystems() {
// Update does nothing, but exists to satisfy the `akara.System` interface
func (m *GameClientBootstrap) Update() {
m.Info("game client bootstrap complete, deactivating")
m.Debug("game client bootstrap complete, deactivating")
m.RemoveSystem(m)
}

View File

@ -34,7 +34,7 @@ type GameConfigSystem struct {
filesToCheck *akara.Subscription
gameConfigs *akara.Subscription
d2components.GameConfigFactory
d2components.FilePathFactory
d2components.FileFactory
d2components.FileTypeFactory
d2components.FileHandleFactory
d2components.FileSourceFactory
@ -48,7 +48,7 @@ func (m *GameConfigSystem) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupFactories()
m.setupSubscriptions()
@ -60,9 +60,9 @@ func (m *GameConfigSystem) setupLogger() {
}
func (m *GameConfigSystem) setupFactories() {
m.Info("setting up component factories")
m.Debug("setting up component factories")
m.InjectComponent(&d2components.FilePath{}, &m.FilePath)
m.InjectComponent(&d2components.File{}, &m.File)
m.InjectComponent(&d2components.FileType{}, &m.FileType)
m.InjectComponent(&d2components.FileHandle{}, &m.FileHandle)
m.InjectComponent(&d2components.FileSource{}, &m.FileSource)
@ -71,12 +71,12 @@ func (m *GameConfigSystem) setupFactories() {
}
func (m *GameConfigSystem) setupSubscriptions() {
m.Info("setting up component subscriptions")
m.Debug("setting up component subscriptions")
// we are going to check entities that dont yet have loaded asset types
filesToCheck := m.NewComponentFilter().
Require(
&d2components.FilePath{},
&d2components.File{},
&d2components.FileType{},
&d2components.FileHandle{},
).
@ -112,7 +112,7 @@ func (m *GameConfigSystem) Update() {
func (m *GameConfigSystem) checkForNewConfig(entities []akara.EID) {
for _, eid := range entities {
fp, found := m.GetFilePath(eid)
fp, found := m.GetFile(eid)
if !found {
continue
}

View File

@ -23,8 +23,8 @@ func Test_GameConfigSystem_Bootstrap(t *testing.T) {
world := akara.NewWorld(cfg)
cfgSys.AddFilePath(world.NewEntity()).Path = testDataPath
cfgSys.AddFilePath(world.NewEntity()).Path = "config.json"
cfgSys.AddFile(world.NewEntity()).Path = testDataPath
cfgSys.AddFile(world.NewEntity()).Path = "config.json"
// at this point the world has initialized the sceneSystems. when the world
// updates it should process the config dir to a source and then

View File

@ -20,6 +20,7 @@ type GameObjectFactory struct {
*d2util.Logger
*SpriteFactory
*ShapeSystem
*UIWidgetFactory
}
// Init will initialize the Game Object Factory by injecting all of the factory subsystems into the world
@ -28,7 +29,7 @@ func (t *GameObjectFactory) Init(world *akara.World) {
t.setupLogger()
t.Info("initializing ...")
t.Debug("initializing ...")
t.injectSubSystems()
}
@ -39,13 +40,15 @@ func (t *GameObjectFactory) setupLogger() {
}
func (t *GameObjectFactory) injectSubSystems() {
t.Info("creating sprite factory")
t.SpriteFactory = NewSpriteFactorySubsystem(t.BaseSystem, t.Logger)
t.Debug("creating sprite factory")
t.SpriteFactory = NewSpriteFactory(t.BaseSystem, t.Logger)
t.ShapeSystem = NewShapeSystem(t.BaseSystem, t.Logger)
t.UIWidgetFactory = NewUIWidgetFactory(t.BaseSystem, t.Logger, t.SpriteFactory, t.ShapeSystem)
}
// Update updates all the sub-sceneSystems
func (t *GameObjectFactory) Update() {
t.SpriteFactory.Update()
t.ShapeSystem.Update()
t.UIWidgetFactory.Update()
}

View File

@ -38,7 +38,7 @@ func (m *InputSystem) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupFactories()
m.setupSubscriptions()
@ -52,14 +52,14 @@ func (m *InputSystem) setupLogger() {
}
func (m *InputSystem) setupFactories() {
m.Info("setting up component factories")
m.Debug("setting up component factories")
m.InjectComponent(&d2components.GameConfig{}, &m.GameConfig)
m.InjectComponent(&d2components.Interactive{}, &m.Interactive)
}
func (m *InputSystem) setupSubscriptions() {
m.Info("setting up component subscriptions")
m.Debug("setting up component subscriptions")
interactives := m.NewComponentFilter().
Require(&d2components.Interactive{}).

View File

@ -33,7 +33,7 @@ func (m *MovementSystem) Init(world *akara.World) {
m.Logger = d2util.NewLogger()
m.SetPrefix(logPrefixMovementSystem)
m.Info("initializing ...")
m.Debug("initializing ...")
m.InjectComponent(&d2components.Transform{}, &m.Transform)
m.InjectComponent(&d2components.Velocity{}, &m.Velocity)

View File

@ -52,7 +52,7 @@ func (m *RenderSystem) Init(world *akara.World) {
m.setupLogger()
m.Info("initializing ...")
m.Debug("initializing ...")
m.setupFactories()
m.setupSubscriptions()
@ -120,7 +120,7 @@ func (m *RenderSystem) Update() {
}
func (m *RenderSystem) createRenderer() {
m.Info("creating renderer instance")
m.Debug("creating renderer instance")
configs := m.configs.GetEntities()
if len(configs) < 1 {
@ -220,7 +220,7 @@ func (m *RenderSystem) updateWorld() error {
}
func (m *RenderSystem) StartGameLoop() error {
m.Infof("starting game loop ...")
m.Info("starting game loop ...")
return m.renderer.Run(m.render, m.updateWorld, 800, 600, gameTitle)
}

View File

@ -110,7 +110,7 @@ func (s *BaseScene) Init(world *akara.World) {
}
func (s *BaseScene) boot() {
s.Info("base scene booting ...")
s.Debug("base scene booting ...")
s.Add = &sceneObjectFactory{
BaseScene: s,
@ -119,47 +119,15 @@ func (s *BaseScene) boot() {
s.Add.SetPrefix(fmt.Sprintf("%s -> %s", s.key, "Object Factory"))
for idx := range s.Systems {
if rendersys, ok := s.Systems[idx].(*RenderSystem); ok && s.sceneSystems.RenderSystem == nil {
s.sceneSystems.RenderSystem = rendersys
continue
}
s.bindRequiredSystems()
if inputSys, ok := s.Systems[idx].(*InputSystem); ok && s.sceneSystems.InputSystem == nil {
s.sceneSystems.InputSystem = inputSys
continue
}
if objFactory, ok := s.Systems[idx].(*GameObjectFactory); ok && s.sceneSystems.GameObjectFactory == nil {
s.sceneSystems.GameObjectFactory = objFactory
continue
}
}
if s.sceneSystems.RenderSystem == nil {
s.Info("waiting for render system ...")
return
}
if s.sceneSystems.RenderSystem.renderer == nil {
s.Info("waiting for renderer instance ...")
return
}
if s.sceneSystems.InputSystem == nil {
s.Info("waiting for input system")
return
}
if s.sceneSystems.GameObjectFactory == nil {
s.Info("waiting for game object factory ...")
if !s.requiredSystemsPresent() {
return
}
s.setupFactories()
s.sceneSystems.SpriteFactory.RenderSystem = s.sceneSystems.RenderSystem
s.sceneSystems.ShapeSystem.RenderSystem = s.sceneSystems.RenderSystem
s.setupSceneObjectFactories()
const (
defaultWidth = 800
@ -168,12 +136,67 @@ func (s *BaseScene) boot() {
s.Add.Viewport(mainViewport, defaultWidth, defaultHeight)
s.Info("base scene booted!")
s.Debug("base scene booted!")
s.booted = true
}
func (s *BaseScene) bindRequiredSystems() {
for idx := range s.Systems {
noRenderSys := s.sceneSystems.RenderSystem == nil
noInputSys := s.sceneSystems.InputSystem == nil
noObjectFactory := s.sceneSystems.GameObjectFactory == nil
sys := s.Systems[idx]
if rendersys, found := sys.(*RenderSystem); found && noRenderSys {
s.sceneSystems.RenderSystem = rendersys
continue
}
if inputSys, found := sys.(*InputSystem); found && noInputSys {
s.sceneSystems.InputSystem = inputSys
continue
}
if objFactory, found := sys.(*GameObjectFactory); found && noObjectFactory {
s.sceneSystems.GameObjectFactory = objFactory
continue
}
}
}
func (s *BaseScene) requiredSystemsPresent() bool {
if s.sceneSystems.RenderSystem == nil {
s.Debug("waiting for render system ...")
return false
}
if s.sceneSystems.RenderSystem.renderer == nil {
s.Debug("waiting for renderer instance ...")
return false
}
if s.sceneSystems.InputSystem == nil {
s.Debug("waiting for input system")
return false
}
if s.sceneSystems.GameObjectFactory == nil {
s.Debug("waiting for game object factory ...")
return false
}
return true
}
func (s *BaseScene) setupSceneObjectFactories() {
s.sceneSystems.SpriteFactory.RenderSystem = s.sceneSystems.RenderSystem
s.sceneSystems.ShapeSystem.RenderSystem = s.sceneSystems.RenderSystem
s.sceneSystems.UIWidgetFactory.RenderSystem = s.sceneSystems.RenderSystem
}
func (s *BaseScene) setupFactories() {
s.Info("setting up component factories")
s.Debug("setting up component factories")
s.InjectComponent(&d2components.MainViewport{}, &s.MainViewport)
s.InjectComponent(&d2components.Viewport{}, &s.Viewport)

View File

@ -47,7 +47,7 @@ type EbitenSplashScene struct {
func (s *EbitenSplashScene) Init(world *akara.World) {
s.World = world
s.Info("initializing ...")
s.Debug("initializing ...")
}
func (s *EbitenSplashScene) boot() {
@ -126,7 +126,7 @@ func (s *EbitenSplashScene) createSplash() {
interactive.InputVector.SetMouseButton(d2input.MouseButtonLeft)
interactive.Callback = func() bool {
s.Info("hiding splash scene")
s.Debug("hiding splash scene")
s.timeElapsed = splashTimeout
@ -151,7 +151,7 @@ func (s *EbitenSplashScene) updateSplash() {
if vpAlpha.Alpha <= 0 {
vpAlpha.Alpha = 0
s.Info("finished, deactivating")
s.Debug("finished, deactivating")
s.SetActive(false)
}
}

View File

@ -1 +0,0 @@
package d2systems

View File

@ -46,7 +46,7 @@ type LoadingScene struct {
func (s *LoadingScene) Init(world *akara.World) {
s.World = world
s.Info("initializing ...")
s.Debug("initializing ...")
s.backgroundColor = color.Black
@ -54,94 +54,51 @@ func (s *LoadingScene) Init(world *akara.World) {
}
func (s *LoadingScene) setupSubscriptions() {
s.Info("setting up component subscriptions")
s.Debug("setting up component subscriptions")
stage1 := s.NewComponentFilter().
Require(
&d2components.FilePath{},
&d2components.File{},
).
Forbid( // but we forbid files that are already loaded
&d2components.FileType{},
&d2components.FileHandle{},
&d2components.FileLoaded{},
&d2components.FileSource{},
&d2components.GameConfig{},
&d2components.StringTable{},
&d2components.DataDictionary{},
&d2components.Palette{},
&d2components.PaletteTransform{},
&d2components.Cof{},
&d2components.Dc6{},
&d2components.Dcc{},
&d2components.Ds1{},
&d2components.Dt1{},
&d2components.Wav{},
&d2components.AnimationData{},
).
Build()
stage2 := s.NewComponentFilter().
Require(
&d2components.FilePath{},
&d2components.File{},
&d2components.FileType{},
).
Forbid( // but we forbid files that are already loaded
&d2components.FileHandle{},
&d2components.FileLoaded{},
&d2components.FileSource{},
&d2components.GameConfig{},
&d2components.StringTable{},
&d2components.DataDictionary{},
&d2components.Palette{},
&d2components.PaletteTransform{},
&d2components.Cof{},
&d2components.Dc6{},
&d2components.Dcc{},
&d2components.Ds1{},
&d2components.Dt1{},
&d2components.Wav{},
&d2components.AnimationData{},
).
Build()
stage3 := s.NewComponentFilter().
Require(
&d2components.FilePath{},
&d2components.File{},
&d2components.FileType{},
&d2components.FileHandle{},
).
Forbid( // but we forbid files that are already loaded
&d2components.FileLoaded{},
&d2components.FileSource{},
&d2components.GameConfig{},
&d2components.StringTable{},
&d2components.DataDictionary{},
&d2components.Palette{},
&d2components.PaletteTransform{},
&d2components.Cof{},
&d2components.Dc6{},
&d2components.Dcc{},
&d2components.Ds1{},
&d2components.Dt1{},
&d2components.Wav{},
&d2components.AnimationData{},
).
Build()
// we want to know about loaded files, too
stage4 := s.NewComponentFilter().
RequireOne(
Require(
&d2components.File{},
&d2components.FileType{},
&d2components.FileHandle{},
&d2components.FileLoaded{},
).
Forbid( // but we forbid files that are already loaded
&d2components.FileSource{},
&d2components.GameConfig{},
&d2components.StringTable{},
&d2components.DataDictionary{},
&d2components.Palette{},
&d2components.PaletteTransform{},
&d2components.Cof{},
&d2components.Dc6{},
&d2components.Dcc{},
&d2components.Ds1{},
&d2components.Dt1{},
&d2components.Wav{},
&d2components.AnimationData{},
).
Build()
@ -163,7 +120,7 @@ func (s *LoadingScene) boot() {
}
func (s *LoadingScene) createLoadingScreen() {
s.Info("creating loading screen")
s.Info("creating loading sprite")
s.loadingSprite = s.Add.Sprite(0, 0, d2resource.LoadingScreen, d2resource.PaletteLoading)
}

View File

@ -54,7 +54,7 @@ type MainMenuScene struct {
func (s *MainMenuScene) Init(world *akara.World) {
s.World = world
s.Info("initializing ...")
s.Debug("initializing ...")
}
func (s *MainMenuScene) boot() {
@ -73,7 +73,7 @@ func (s *MainMenuScene) boot() {
}
func (s *MainMenuScene) setupViewports() {
s.Info("setting up viewports")
s.Debug("setting up viewports")
imgPath := d2resource.GameSelectScreen
palPath := d2resource.PaletteSky
@ -82,7 +82,7 @@ func (s *MainMenuScene) setupViewports() {
}
func (s *MainMenuScene) createBackground() {
s.Info("creating background")
s.Debug("creating background")
imgPath := d2resource.GameSelectScreen
palPath := d2resource.PaletteSky
@ -91,7 +91,7 @@ func (s *MainMenuScene) createBackground() {
}
func (s *MainMenuScene) createLogo() {
s.Info("creating logo")
s.Debug("creating logo")
const (
logoX, logoY = 400, 120
@ -108,11 +108,11 @@ func (s *MainMenuScene) createLogo() {
}
func (s *MainMenuScene) createButtons() {
s.Info("creating buttons")
s.Debug("creating buttons")
}
func (s *MainMenuScene) createTrademarkScreen() {
s.Info("creating trademark screen")
s.Debug("creating trademark screen")
imgPath := d2resource.TrademarkScreen
palPath := d2resource.PaletteSky
@ -124,7 +124,7 @@ func (s *MainMenuScene) createTrademarkScreen() {
interactive.InputVector.SetMouseButton(d2input.MouseButtonLeft)
interactive.Callback = func() bool {
s.Info("hiding trademark sprite")
s.Debug("hiding trademark sprite")
alpha := s.AddAlpha(s.sprites.trademark)
@ -166,7 +166,7 @@ func (s *MainMenuScene) Update() {
}
if !s.logoInit {
s.Info("attempting logo sprite init")
s.Debug("attempting logo sprite init")
s.initLogoSprites()
}
@ -201,7 +201,7 @@ func (s *MainMenuScene) initLogoSprites() {
}
}
s.Info("initializing logo sprites")
s.Debug("initializing logo sprites")
for _, id := range logoSprites {
sprite, _ := s.GetSprite(id)

View File

@ -44,7 +44,7 @@ type MouseCursorScene struct {
func (s *MouseCursorScene) Init(world *akara.World) {
s.World = world
s.Info("initializing ...")
s.Debug("initializing ...")
}
func (s *MouseCursorScene) boot() {
@ -61,7 +61,7 @@ func (s *MouseCursorScene) boot() {
}
func (s *MouseCursorScene) createMouseCursor() {
s.Info("creating mouse cursor")
s.Debug("creating mouse cursor")
s.cursor = s.Add.Sprite(0, 0, d2resource.CursorDefault, d2resource.PaletteUnits)
}
@ -136,7 +136,7 @@ func (s *MouseCursorScene) registerTerminalCommands() {
}
func (s *MouseCursorScene) registerDebugCommand() {
s.Info("registering debug command")
s.Debug("registering debug command")
const (
command = "debug_mouse"

View File

@ -87,3 +87,30 @@ func (s *sceneObjectFactory) Rectangle(x, y, width, height int, c color.Color) a
return eid
}
func (s *sceneObjectFactory) Button(x, y float64, imgPath, palPath string) akara.EID {
s.Debug("creating button")
eid := s.sceneSystems.UIWidgetFactory.Button(x, y, imgPath, palPath)
s.addBasicComponents(eid)
transform := s.AddTransform(eid)
transform.Translation.X, transform.Translation.Y = float64(x), float64(y)
s.SceneObjects = append(s.SceneObjects, eid)
return eid
}
func (s *sceneObjectFactory) Label(fontPath, spritePath, palettePath string) akara.EID {
s.Debug("creating label")
eid := s.sceneSystems.UIWidgetFactory.Label(fontPath, spritePath, palettePath)
s.addBasicComponents(eid)
s.SceneObjects = append(s.SceneObjects, eid)
return eid
}

View File

@ -3,7 +3,7 @@ package d2systems
const (
scenePriorityMainMenu = iota
scenePriorityLoading
scenePriorityTerminal
scenePriorityMouseCursor
scenePriorityTerminal
scenePriorityEbitenSplash
)

View File

@ -44,7 +44,7 @@ type ShapeSystem struct {
func (t *ShapeSystem) Init(world *akara.World) {
t.World = world
t.Info("initializing sprite factory ...")
t.Debug("initializing sprite factory ...")
t.setupFactories()
t.setupSubscriptions()

View File

@ -1,6 +1,8 @@
package d2systems
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
"github.com/gravestench/akara"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
@ -13,11 +15,16 @@ const (
fmtCreateSpriteErr = "could not create sprite from image `%s` and palette `%s`"
)
// NewSpriteFactorySubsystem creates a new sprite factory which is intended
const (
spriteCacheBudget = 1024
)
// NewSpriteFactory creates a new sprite factory which is intended
// to be embedded in the game object factory system.
func NewSpriteFactorySubsystem(b akara.BaseSystem, l *d2util.Logger) *SpriteFactory {
func NewSpriteFactory(b akara.BaseSystem, l *d2util.Logger) *SpriteFactory {
sys := &SpriteFactory{
Logger: l,
cache: d2cache.CreateCache(spriteCacheBudget),
}
sys.BaseSystem = b
@ -33,13 +40,13 @@ type spriteLoadQueueEntry struct {
type spriteLoadQueue = map[akara.EID]spriteLoadQueueEntry
// SpriteFactory is responsible for queueing sprites to be loaded (as spriteations),
// SpriteFactory is responsible for queueing sprites to be loaded (as sprites),
// as well as binding the spriteation to a renderer if one is present (which generates the sprite surfaces).
type SpriteFactory struct {
akara.BaseSubscriberSystem
*d2util.Logger
RenderSystem *RenderSystem
d2components.FilePathFactory
d2components.FileFactory
d2components.TransformFactory
d2components.Dc6Factory
d2components.DccFactory
@ -51,13 +58,14 @@ type SpriteFactory struct {
loadQueue spriteLoadQueue
spritesToRender *akara.Subscription
spritesToUpdate *akara.Subscription
cache d2interface.Cache
}
// Init the sprite factory, injecting the necessary components
func (t *SpriteFactory) Init(world *akara.World) {
t.World = world
t.Info("initializing sprite factory ...")
t.Debug("initializing sprite factory ...")
t.setupFactories()
t.setupSubscriptions()
@ -66,7 +74,7 @@ func (t *SpriteFactory) Init(world *akara.World) {
}
func (t *SpriteFactory) setupFactories() {
t.InjectComponent(&d2components.FilePath{}, &t.FilePath)
t.InjectComponent(&d2components.File{}, &t.File)
t.InjectComponent(&d2components.Transform{}, &t.Transform)
t.InjectComponent(&d2components.Dc6{}, &t.Dc6)
t.InjectComponent(&d2components.Dcc{}, &t.Dcc)
@ -79,12 +87,12 @@ func (t *SpriteFactory) setupFactories() {
func (t *SpriteFactory) setupSubscriptions() {
spritesToRender := t.NewComponentFilter().
Require(&d2components.Sprite{}). // we want to process entities that have an spriteation ...
Require(&d2components.Sprite{}). // we want to process entities that have an sprite ...
Forbid(&d2components.Texture{}). // ... but are missing a surface
Build()
spritesToUpdate := t.NewComponentFilter().
Require(&d2components.Sprite{}). // we want to process entities that have an spriteation ...
Require(&d2components.Sprite{}). // we want to process entities that have an sprite ...
Require(&d2components.Texture{}). // ... but are missing a surface
Build()
@ -92,8 +100,8 @@ func (t *SpriteFactory) setupSubscriptions() {
t.spritesToUpdate = t.AddSubscription(spritesToUpdate)
}
// Update processes the load queue which attempting to create spriteations, as well as
// binding existing spriteations to a renderer if one is present.
// Update processes the load queue which attempting to create sprites, as well as
// binding existing sprites to a renderer if one is present.
func (t *SpriteFactory) Update() {
for spriteID := range t.loadQueue {
t.tryCreatingSprite(spriteID)
@ -116,8 +124,8 @@ func (t *SpriteFactory) Sprite(x, y float64, imgPath, palPath string) akara.EID
transform.Translation.X, transform.Translation.Y = x, y
imgID, palID := t.NewEntity(), t.NewEntity()
t.AddFilePath(imgID).Path = imgPath
t.AddFilePath(palID).Path = palPath
t.AddFile(imgID).Path = imgPath
t.AddFile(palID).Path = palPath
t.loadQueue[spriteID] = spriteLoadQueueEntry{
spriteImage: imgID,
@ -144,12 +152,12 @@ func (t *SpriteFactory) tryCreatingSprite(id akara.EID) {
entry := t.loadQueue[id]
imageID, paletteID := entry.spriteImage, entry.spritePalette
imagePath, found := t.GetFilePath(imageID)
imageFile, found := t.GetFile(imageID)
if !found {
return
}
palettePath, found := t.GetFilePath(paletteID)
paletteFile, found := t.GetFile(paletteID)
if !found {
return
}
@ -163,23 +171,33 @@ func (t *SpriteFactory) tryCreatingSprite(id akara.EID) {
var err error
if dc6, found := t.GetDc6(imageID); found {
sprite, err = t.createDc6Sprite(dc6, palette)
cacheKey := spriteCacheKey(imageFile.Path, paletteFile.Path)
if iface, found := t.cache.Retrieve(cacheKey); found {
sprite = iface.(d2interface.Sprite)
}
if dcc, found := t.GetDcc(imageID); found {
if dc6, found := t.GetDc6(imageID); found && sprite == nil {
sprite, err = t.createDc6Sprite(dc6, palette)
_ = t.cache.Insert(cacheKey, sprite, 1)
}
if dcc, found := t.GetDcc(imageID); found && sprite == nil {
sprite, err = t.createDccSprite(dcc, palette)
_ = t.cache.Insert(cacheKey, sprite, 1)
}
if err != nil {
t.Errorf(fmtCreateSpriteErr, imagePath.Path, palettePath.Path)
t.Errorf(fmtCreateSpriteErr, imageFile.Path, paletteFile.Path)
t.RemoveEntity(id)
t.RemoveEntity(imageID)
t.RemoveEntity(paletteID)
}
t.AddSprite(id).Sprite = sprite
spriteComponent := t.AddSprite(id)
spriteComponent.Sprite = sprite
spriteComponent.SpritePath = imageFile.Path
spriteComponent.PalettePath = paletteFile.Path
delete(t.loadQueue, id)
}
@ -263,3 +281,7 @@ func (t *SpriteFactory) createDccSprite(
) (d2interface.Sprite, error) {
return d2sprite.NewDCCSprite(dcc.DCC, pal.Palette, 0)
}
func spriteCacheKey(imgpath, palpath string) string {
return fmt.Sprintf("%s::%s", imgpath, palpath)
}

View File

@ -51,13 +51,13 @@ type TerminalScene struct {
func (s *TerminalScene) Init(world *akara.World) {
s.World = world
s.Info("initializing ...")
s.Debug("initializing ...")
s.setupSubscriptions()
}
func (s *TerminalScene) setupSubscriptions() {
s.Info("setting up component subscriptions")
s.Debug("setting up component subscriptions")
commandsToRegister := s.NewComponentFilter().
Require(

View File

@ -0,0 +1,112 @@
package d2systems
import (
"image/color"
"math/rand"
"github.com/gravestench/akara"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
)
const (
sceneKeyLabelTest = "Label Test Scene"
)
// NewLabelTestScene creates a new main menu scene. This is the first screen that the user
// will see when launching the game.
func NewLabelTestScene() *LabelTestScene {
scene := &LabelTestScene{
BaseScene: NewBaseScene(sceneKeyLabelTest),
}
return scene
}
// static check that LabelTestScene implements the scene interface
var _ d2interface.Scene = &LabelTestScene{}
// LabelTestScene represents the game's main menu, where users can select single or multi player,
// or start the map engine test.
type LabelTestScene struct {
*BaseScene
booted bool
labels *akara.Subscription
}
// Init the main menu scene
func (s *LabelTestScene) Init(world *akara.World) {
s.World = world
labels := s.World.NewComponentFilter().Require(&d2components.Label{}).Build()
s.labels = s.World.AddSubscription(labels)
s.Debug("initializing ...")
}
func (s *LabelTestScene) boot() {
if !s.BaseScene.booted {
s.BaseScene.boot()
return
}
s.createLabels()
s.booted = true
}
func (s *LabelTestScene) createLabels() {
for idx := 0; idx < 1000; idx++ {
l := s.Add.Label("LOLWUT", d2resource.Font24, d2resource.PaletteStatic)
trs := s.AddTransform(l)
trs.Translation.Set(rand.Float64()*800, rand.Float64()*600, 1)
}
}
// Update the main menu scene
func (s *LabelTestScene) Update() {
if s.Paused() {
return
}
for _, eid := range s.labels.GetEntities() {
//s.setLabelBackground(eid)
s.updatePosition(eid)
}
if !s.booted {
s.boot()
}
s.BaseScene.Update()
}
func (s *LabelTestScene) setLabelBackground(eid akara.EID) {
label, found := s.GetLabel(eid)
if !found {
return
}
label.SetBackgroundColor(color.Black)
}
func (s *LabelTestScene) updatePosition(eid akara.EID) {
trs, found := s.GetTransform(eid)
if !found {
return
}
x, y, z := trs.Translation.AddScalar(1).XYZ()
if x > 800 {
x -= 800
}
if y > 600 {
y -= 600
}
trs.Translation.Set(x, y, z)
}

View File

@ -0,0 +1,300 @@
package d2systems
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2bitmapfont"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
"github.com/gravestench/akara"
"image/color"
)
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 {
sprite, palette 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
d2components.FileFactory
d2components.TransformFactory
d2components.InteractiveFactory
d2components.FontTableFactory
d2components.PaletteFactory
d2components.BitmapFontFactory
d2components.LabelFactory
labelsToUpdate *akara.Subscription
booted bool
}
// 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.File)
t.InjectComponent(&d2components.Transform{}, &t.Transform)
t.InjectComponent(&d2components.Interactive{}, &t.Interactive)
t.InjectComponent(&d2components.FontTable{}, &t.FontTable)
t.InjectComponent(&d2components.Palette{}, &t.Palette)
t.InjectComponent(&d2components.BitmapFont{}, &t.BitmapFont)
t.InjectComponent(&d2components.Label{}, &t.LabelFactory.Label)
}
func (t *UIWidgetFactory) setupSubscriptions() {
labelsToUpdate := t.NewComponentFilter().
Require(&d2components.Label{}).
Build()
t.labelsToUpdate = t.AddSubscription(labelsToUpdate)
}
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() {
if !t.booted {
t.boot()
return
}
for labelEID := range t.labelLoadQueue {
t.processLabel(labelEID)
}
for _, labelEID := range t.labelsToUpdate.GetEntities() {
t.renderLabel(labelEID)
}
}
// Label creates a label widget.
//
// The font is assumed to be a path for two files, omiting 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.AddFile(tableEID).Path = tablePath
spriteEID := t.SpriteFactory.Sprite(0, 0, spritePath, palettePath)
t.labelLoadQueue[labelEID] = labelLoadQueueEntry{
table: tableEID,
sprite: spriteEID,
}
label := t.AddLabel(labelEID)
label.SetText(text)
return labelEID
}
// Label creates a label widget
func (t *UIWidgetFactory) processLabel(labelEID akara.EID) {
bmfComponent, found := t.GetBitmapFont(labelEID)
if !found {
t.addBitmapFontForLabel(labelEID)
return
}
bmfComponent.Sprite.BindRenderer(t.renderer)
label, found := t.GetLabel(labelEID)
if !found {
label = t.AddLabel(labelEID)
}
label.Font = bmfComponent.BitmapFont
t.RemoveEntity(t.labelLoadQueue[labelEID].table)
delete(t.labelLoadQueue, labelEID)
}
func (t *UIWidgetFactory) renderLabel(labelEID akara.EID) {
label, found := t.GetLabel(labelEID)
if !found {
return
}
bmf, found := t.GetBitmapFont(labelEID)
if !found {
return
}
if label.Font != bmf.BitmapFont {
label.Font = bmf.BitmapFont
}
if !label.IsDirty() {
return
}
texture, found := t.RenderSystem.GetTexture(labelEID)
if !found {
texture = t.RenderSystem.AddTexture(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.GetFontTable(entry.table)
_, spriteFound := t.GetSprite(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.GetFile(entry.table)
if !found {
return
}
sprite, found := t.GetSprite(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.AddBitmapFont(labelEID).BitmapFont = iface.(*d2bitmapfont.BitmapFont)
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.AddBitmapFont(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.GetFontTable(entry.table)
sprite, spriteFound := t.GetSprite(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)
}
// Button creates a button ui widget
func (t *UIWidgetFactory) Button(x, y float64, imgPath, palPath string) akara.EID {
buttonEID := t.NewEntity()
//transform := t.AddTransform(buttonEID)
//transform.Translation.X, transform.Translation.Y = x, y
//
//imgID, palID := t.NewEntity(), t.NewEntity()
//t.AddFile(imgID).Path = imgPath
//t.AddFile(palID).Path = palPath
//
//t.buttonLoadQueue[buttonEID] = buttonLoadQueueEntry{
// spriteImage: imgID,
// spritePalette: palID,
//}
return buttonEID
}

View File

@ -38,7 +38,7 @@ func (t *TimeScaleSystem) Init(world *akara.World) {
t.Logger = d2util.NewLogger()
t.SetPrefix(logPrefixTimeScaleSystem)
t.Info("initializing ...")
t.Debug("initializing ...")
t.InjectComponent(&d2components.CommandRegistration{}, &t.CommandRegistration)
t.InjectComponent(&d2components.Dirty{}, &t.Dirty)

View File

@ -30,7 +30,7 @@ func (u *UpdateCounter) Init(world *akara.World) {
u.SetActive(false)
}
u.Info("initializing")
u.Debug("initializing")
}
func (u *UpdateCounter) setupLogger() {

View File

@ -711,6 +711,11 @@ func getButtonLayouts() map[ButtonType]ButtonLayout {
}
}
// GetButtonLayout returns a button layout for the given button type
func GetButtonLayout(t ButtonType) ButtonLayout {
return getButtonLayouts()[t]
}
var _ Widget = &Button{} // static check to ensure button implements widget
// Button defines a standard wide UI button

View File

@ -5,10 +5,11 @@ import "fmt"
// ColorToken is a string which is used inside of label strings to set font color.
type ColorToken string
// Color token formatting and pattern matching utility strings
const (
colorTokenFmt = `%s%s`
colorTokenMatch = `\[[^\]]+\]` // nolint:gosec // has nothing to to with credentials
colorStrMatch = colorTokenMatch + `[^\[]+`
ColorTokenFmt = `%s%s`
ColorTokenMatch = `\[[^\]]+\]` // nolint:gosec // has nothing to to with credentials
ColorStrMatch = ColorTokenMatch + `[^\[]+`
)
// Color tokens for colored labels
@ -41,18 +42,18 @@ const (
)
const (
colorGrey100Alpha = 0x69_69_69_ff
colorWhite100Alpha = 0xff_ff_ff_ff
colorBlue100Alpha = 0x69_69_ff_ff
colorYellow100Alpha = 0xff_ff_64_ff
colorGreen100Alpha = 0x00_ff_00_ff
colorGold100Alpha = 0xc7_b3_77_ff
colorOrange100Alpha = 0xff_a8_00_ff
colorRed100Alpha = 0xff_77_77_ff
colorBlack100Alpha = 0x00_00_00_ff
ColorGrey100Alpha = 0x69_69_69_ff
ColorWhite100Alpha = 0xff_ff_ff_ff
ColorBlue100Alpha = 0x69_69_ff_ff
ColorYellow100Alpha = 0xff_ff_64_ff
ColorGreen100Alpha = 0x00_ff_00_ff
ColorGold100Alpha = 0xc7_b3_77_ff
ColorOrange100Alpha = 0xff_a8_00_ff
ColorRed100Alpha = 0xff_77_77_ff
ColorBlack100Alpha = 0x00_00_00_ff
)
// ColorTokenize formats the string with the given color token
func ColorTokenize(s string, t ColorToken) string {
return fmt.Sprintf(colorTokenFmt, t, s)
return fmt.Sprintf(ColorTokenFmt, t, s)
}

View File

@ -114,8 +114,8 @@ func (v *Label) SetBackgroundColor(c color.Color) {
}
func (v *Label) processColorTokens(str string) string {
tokenMatch := regexp.MustCompile(colorTokenMatch)
tokenStrMatch := regexp.MustCompile(colorStrMatch)
tokenMatch := regexp.MustCompile(ColorTokenMatch)
tokenStrMatch := regexp.MustCompile(ColorStrMatch)
empty := []byte("")
tokenPosition := 0
@ -175,15 +175,15 @@ func (v *Label) Advance(elapsed float64) error {
func getColor(token ColorToken) color.Color {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/823
colors := map[ColorToken]color.Color{
ColorTokenGrey: d2util.Color(colorGrey100Alpha),
ColorTokenWhite: d2util.Color(colorWhite100Alpha),
ColorTokenBlue: d2util.Color(colorBlue100Alpha),
ColorTokenYellow: d2util.Color(colorYellow100Alpha),
ColorTokenGreen: d2util.Color(colorGreen100Alpha),
ColorTokenGold: d2util.Color(colorGold100Alpha),
ColorTokenOrange: d2util.Color(colorOrange100Alpha),
ColorTokenRed: d2util.Color(colorRed100Alpha),
ColorTokenBlack: d2util.Color(colorBlack100Alpha),
ColorTokenGrey: d2util.Color(ColorGrey100Alpha),
ColorTokenWhite: d2util.Color(ColorWhite100Alpha),
ColorTokenBlue: d2util.Color(ColorBlue100Alpha),
ColorTokenYellow: d2util.Color(ColorYellow100Alpha),
ColorTokenGreen: d2util.Color(ColorGreen100Alpha),
ColorTokenGold: d2util.Color(ColorGold100Alpha),
ColorTokenOrange: d2util.Color(ColorOrange100Alpha),
ColorTokenRed: d2util.Color(ColorRed100Alpha),
ColorTokenBlack: d2util.Color(ColorBlack100Alpha),
}
chosen := colors[token]

2
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4 // indirect
github.com/go-restruct/restruct v1.2.0-alpha
github.com/google/uuid v1.1.2
github.com/gravestench/akara v0.0.0-20201206061149-9be03b4110f2
github.com/gravestench/akara v0.0.0-20201208183338-ab0934060133
github.com/gravestench/pho v0.0.0-20201029002250-f9afbd637e4d
github.com/hajimehoshi/ebiten/v2 v2.0.1
github.com/pkg/profile v1.5.0

2
go.sum
View File

@ -23,6 +23,8 @@ github.com/gravestench/akara v0.0.0-20201203202918-85b8a01d1130 h1:09fkM2hfORgZJ
github.com/gravestench/akara v0.0.0-20201203202918-85b8a01d1130/go.mod h1:fTeda1SogMg5Lkd4lXMEd/Pk/a5/gQuLGaAI2rn1PBQ=
github.com/gravestench/akara v0.0.0-20201206061149-9be03b4110f2 h1:mOIIK6AgIyaEslKsu+tsguzFWaMLGjlMuUKqOlABhGk=
github.com/gravestench/akara v0.0.0-20201206061149-9be03b4110f2/go.mod h1:fTeda1SogMg5Lkd4lXMEd/Pk/a5/gQuLGaAI2rn1PBQ=
github.com/gravestench/akara v0.0.0-20201208183338-ab0934060133 h1:P9XM5k63US1EavI+23k9GY84xNRnRtg0sT9rlCr4ew4=
github.com/gravestench/akara v0.0.0-20201208183338-ab0934060133/go.mod h1:fTeda1SogMg5Lkd4lXMEd/Pk/a5/gQuLGaAI2rn1PBQ=
github.com/gravestench/pho v0.0.0-20201029002250-f9afbd637e4d h1:CP+/y9SAdv9LifYvicxYdQNmzugykEahAiUhYolMROM=
github.com/gravestench/pho v0.0.0-20201029002250-f9afbd637e4d/go.mod h1:yi5GHMLLWtHhs9tz3q1csUlgGKz5MhZoJcxV8NFBtkk=
github.com/hajimehoshi/bitmapfont/v2 v2.1.0/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs=