mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-09 10:06:35 -05:00
eminary ECS Implementation work
Added a implementation of an Entity Component System (ECS) architecture
This commit is contained in:
parent
3f8dcf2232
commit
b1bf6993d2
23
d2common/d2enum/file_types.go
Normal file
23
d2common/d2enum/file_types.go
Normal file
@ -0,0 +1,23 @@
|
||||
package d2enum
|
||||
|
||||
// FileType represents the type of an asset
|
||||
type FileType int
|
||||
|
||||
// File types
|
||||
const (
|
||||
FileTypeUnknown FileType = iota
|
||||
FileTypeDirectory
|
||||
FileTypeMPQ
|
||||
FileTypeJSON
|
||||
FileTypeStringTable
|
||||
FileTypeDataDictionary
|
||||
FileTypePalette
|
||||
FileTypePaletteTransform
|
||||
FileTypeCOF
|
||||
FileTypeDC6
|
||||
FileTypeDCC
|
||||
FileTypeDS1
|
||||
FileTypeDT1
|
||||
FileTypeWAV
|
||||
FileTypeD2
|
||||
)
|
91
d2common/d2events/d2event_test.go
Normal file
91
d2common/d2events/d2event_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
package d2events
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_EventEmitter_On(t *testing.T) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
ee := NewEventEmitter()
|
||||
|
||||
eventX := "x only"
|
||||
eventY := "y only"
|
||||
eventBoth := "both"
|
||||
|
||||
var x, y int
|
||||
|
||||
ee.On(eventX, func(args ...interface{}) {
|
||||
x++
|
||||
})
|
||||
|
||||
ee.On(eventY, func(args ...interface{}) {
|
||||
y++
|
||||
})
|
||||
|
||||
ee.On(eventBoth, func(args ...interface{}) {
|
||||
ee.Emit(eventX)
|
||||
ee.Emit(eventY)
|
||||
})
|
||||
|
||||
ee.Emit(eventX)
|
||||
|
||||
if x != 1 {
|
||||
t.Error("listener function not called")
|
||||
}
|
||||
|
||||
if y != 0 {
|
||||
t.Error("listener function incorrectly called")
|
||||
}
|
||||
|
||||
ee.Emit(eventY)
|
||||
ee.Emit(eventY)
|
||||
|
||||
if x != 1 {
|
||||
t.Error("listener function incorrectly called")
|
||||
}
|
||||
|
||||
if y != 2 {
|
||||
t.Error("listener function not called")
|
||||
}
|
||||
|
||||
ee.Emit(eventBoth)
|
||||
|
||||
if x != 2 {
|
||||
t.Error("listener function not called")
|
||||
}
|
||||
|
||||
if y != 3 {
|
||||
t.Error("listener function not called")
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_EventEmitter(b *testing.B) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
ee := NewEventEmitter()
|
||||
|
||||
e1 := "testing"
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
for idx := 0; idx < b.N; idx++ {
|
||||
fn := func(args ...interface{}) {
|
||||
args[0].(*sync.WaitGroup).Done()
|
||||
}
|
||||
|
||||
ee.Once(e1, fn)
|
||||
wg.Add(1)
|
||||
}
|
||||
|
||||
ee.Emit(e1, wg)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(ee.listeners) > 0 {
|
||||
b.Error("listener count should be 0")
|
||||
}
|
||||
}
|
11
d2common/d2events/d2events.go
Normal file
11
d2common/d2events/d2events.go
Normal file
@ -0,0 +1,11 @@
|
||||
package d2events
|
||||
|
||||
// NewEventEmitter initializes and returns an EventEmitter instance
|
||||
func NewEventEmitter() *EventEmitter {
|
||||
ee := &EventEmitter{
|
||||
listeners: make(map[string][]*EventListener),
|
||||
count: 0,
|
||||
}
|
||||
|
||||
return ee
|
||||
}
|
133
d2common/d2events/event_emitter.go
Normal file
133
d2common/d2events/event_emitter.go
Normal file
@ -0,0 +1,133 @@
|
||||
package d2events
|
||||
|
||||
type EventEmitter struct {
|
||||
listeners map[string][]*EventListener
|
||||
count int
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) Emit(event string, args ...interface{}) {
|
||||
listeners := ee.listeners[event]
|
||||
|
||||
if listeners == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for idx := range listeners {
|
||||
if listeners[idx].fn != nil {
|
||||
listeners[idx].fn(args...)
|
||||
}
|
||||
|
||||
if listeners[idx].once {
|
||||
listeners = append(listeners[:idx], listeners[idx+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) On(event string, fn func(...interface{})) {
|
||||
ee.addListener(event, fn, false)
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) Off(event string, fn func(...interface{})) {
|
||||
ee.removeListener(event, fn)
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) Once(event string, fn func(...interface{})) {
|
||||
ee.addListener(event, fn, true)
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) addListener(event string, fn func(...interface{}), once bool) *EventEmitter {
|
||||
if fn == nil {
|
||||
return ee
|
||||
}
|
||||
|
||||
listener := &EventListener{fn, once}
|
||||
|
||||
if ee.listeners[event] == nil {
|
||||
ee.listeners[event] = []*EventListener{listener}
|
||||
} else {
|
||||
ee.listeners[event] = append(ee.listeners[event], listener)
|
||||
}
|
||||
|
||||
return ee
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) removeListener(event string, fn func(...interface{})) {
|
||||
listeners := ee.listeners[event]
|
||||
|
||||
if listeners == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for idx := range listeners {
|
||||
listenerFn := &listeners[idx].fn
|
||||
removeFn := &fn
|
||||
if listenerFn == removeFn {
|
||||
ee.listeners[event] = append(listeners[:idx], listeners[idx+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) eventNames() []string {
|
||||
names := make([]string, len(ee.listeners))
|
||||
|
||||
idx := 0
|
||||
|
||||
for event := range ee.listeners {
|
||||
names[idx] = event
|
||||
idx++
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) clearEvent(event string) {
|
||||
ee.count--
|
||||
if ee.count <= 0 {
|
||||
ee.count = 0
|
||||
ee.listeners = make(map[string][]*EventListener)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
delete(ee.listeners, event)
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) getHandlers(event string) []func(...interface{}) {
|
||||
handlers := make([]func(...interface{}), 0)
|
||||
listeners := ee.listeners[event]
|
||||
|
||||
if listeners != nil {
|
||||
for idx := range listeners {
|
||||
handlers = append(handlers, listeners[idx].fn)
|
||||
}
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) getHandlerCount(event string) int {
|
||||
if ee.listeners == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
listeners := ee.listeners[event]
|
||||
if listeners == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(listeners)
|
||||
}
|
||||
|
||||
func (ee *EventEmitter) removeAllListeners(events ...string) {
|
||||
if events != nil {
|
||||
if len(events) > 0 {
|
||||
for idx := range events {
|
||||
ee.clearEvent(events[idx])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ee.listeners = make(map[string][]*EventListener, 0)
|
||||
}
|
6
d2common/d2events/event_listener.go
Normal file
6
d2common/d2events/event_listener.go
Normal file
@ -0,0 +1,6 @@
|
||||
package d2events
|
||||
|
||||
type EventListener struct {
|
||||
fn func(...interface{})
|
||||
once bool
|
||||
}
|
39
d2core/d2asset/asset_manager_test.go
Normal file
39
d2core/d2asset/asset_manager_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package d2asset
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader"
|
||||
)
|
||||
|
||||
func TestAssetManager_LoadFile_NoSource(t *testing.T) {
|
||||
am := &AssetManager{
|
||||
loader: d2loader.NewLoader(nil),
|
||||
tables: d2cache.CreateCache(tableBudget),
|
||||
animations: d2cache.CreateCache(animationBudget),
|
||||
fonts: d2cache.CreateCache(fontBudget),
|
||||
palettes: d2cache.CreateCache(paletteBudget),
|
||||
transforms: d2cache.CreateCache(paletteTransformBudget),
|
||||
}
|
||||
|
||||
_, err := am.LoadFile("an/invalid/path")
|
||||
if err == nil {
|
||||
t.Error("asset manager loaded a file for which there is no source")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAssetManager_LoadFile_NoSource(b *testing.B) {
|
||||
am := &AssetManager{
|
||||
loader: d2loader.NewLoader(nil),
|
||||
tables: d2cache.CreateCache(tableBudget),
|
||||
animations: d2cache.CreateCache(animationBudget),
|
||||
fonts: d2cache.CreateCache(fontBudget),
|
||||
palettes: d2cache.CreateCache(paletteBudget),
|
||||
transforms: d2cache.CreateCache(paletteTransformBudget),
|
||||
}
|
||||
|
||||
for idx := 0; idx < b.N; idx++ {
|
||||
_, _ = am.LoadFile("an/invalid/path")
|
||||
}
|
||||
}
|
27
d2core/d2components/all_component_ids.go
Normal file
27
d2core/d2components/all_component_ids.go
Normal file
@ -0,0 +1,27 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
)
|
||||
|
||||
// Component type ID's
|
||||
const (
|
||||
GameConfigCID ecs.ComponentID = iota
|
||||
FilePathCID
|
||||
FileTypeCID
|
||||
FileSourceCID
|
||||
FileHandleCID
|
||||
AssetStringTableCID
|
||||
AssetDataDictionaryCID
|
||||
AssetPaletteCID
|
||||
AssetPaletteTransformCID
|
||||
AssetCofCID
|
||||
AssetDc6CID
|
||||
AssetDccCID
|
||||
AssetDs1CID
|
||||
AssetDt1CID
|
||||
AssetWavCID
|
||||
AssetD2CID
|
||||
PositionCID
|
||||
VelocityCID
|
||||
)
|
4
d2core/d2components/asset_string_table.go
Normal file
4
d2core/d2components/asset_string_table.go
Normal file
@ -0,0 +1,4 @@
|
||||
package d2components
|
||||
|
||||
type StringTableAsset struct {
|
||||
}
|
101
d2core/d2components/file_handle.go
Normal file
101
d2core/d2components/file_handle.go
Normal file
@ -0,0 +1,101 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||
)
|
||||
|
||||
// static check that FileHandleComponent implements Component
|
||||
var _ ecs.Component = &FileHandleComponent{}
|
||||
|
||||
// static check that FileHandleMap implements ComponentMap
|
||||
var _ ecs.ComponentMap = &FileHandleMap{}
|
||||
|
||||
// FileHandleComponent is a component that contains a data stream
|
||||
type FileHandleComponent struct {
|
||||
Data d2interface.DataStream
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FileHandleComponent) ID() ecs.ComponentID {
|
||||
return FileHandleCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FileHandleComponent) NewMap() ecs.ComponentMap {
|
||||
return NewFileHandleMap()
|
||||
}
|
||||
|
||||
// FileHandle is a convenient reference to be used as a component identifier
|
||||
var FileHandle = (*FileHandleComponent)(nil) // nolint:gochecknoglobals // global by design
|
||||
|
||||
// NewFileHandleMap creates a new map of entity ID's to FileHandleComponent components
|
||||
func NewFileHandleMap() *FileHandleMap {
|
||||
cm := &FileHandleMap{
|
||||
components: make(map[ecs.EID]*FileHandleComponent),
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// FileHandleMap is a map of entity ID's to FileHandleComponent components
|
||||
type FileHandleMap struct {
|
||||
world *ecs.World
|
||||
components map[ecs.EID]*FileHandleComponent
|
||||
}
|
||||
|
||||
// Init initializes the component map with the given world
|
||||
func (cm *FileHandleMap) Init(world *ecs.World) {
|
||||
cm.world = world
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FileHandleMap) ID() ecs.ComponentID {
|
||||
return FileHandleCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FileHandleMap) NewMap() ecs.ComponentMap {
|
||||
return NewFileHandleMap()
|
||||
}
|
||||
|
||||
// Add a new FileHandleComponent for the given entity id, return that component.
|
||||
// If the entity already has a component, just return that one.
|
||||
func (cm *FileHandleMap) Add(id ecs.EID) ecs.Component {
|
||||
if com, has := cm.components[id]; has {
|
||||
return com
|
||||
}
|
||||
|
||||
cm.components[id] = &FileHandleComponent{Data: nil}
|
||||
|
||||
cm.world.UpdateEntity(id)
|
||||
|
||||
return cm.components[id]
|
||||
}
|
||||
|
||||
// AddFileHandle adds a new FileHandleComponent for the given entity id and returns it.
|
||||
// If the entity already has a FileHandleComponent, just return that one.
|
||||
// this is a convenience method for the generic Add method, as it returns a
|
||||
// *FileHandleComponent instead of an ecs.Component
|
||||
func (cm *FileHandleMap) AddFileHandle(id ecs.EID) *FileHandleComponent {
|
||||
return cm.Add(id).(*FileHandleComponent)
|
||||
}
|
||||
|
||||
// Get returns the component associated with the given entity id
|
||||
func (cm *FileHandleMap) Get(id ecs.EID) (ecs.Component, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetFileHandle returns the FileHandleComponent component associated with the given entity id
|
||||
func (cm *FileHandleMap) GetFileHandle(id ecs.EID) (*FileHandleComponent, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// Remove a component for the given entity id, return the component.
|
||||
func (cm *FileHandleMap) Remove(id ecs.EID) {
|
||||
delete(cm.components, id)
|
||||
cm.world.UpdateEntity(id)
|
||||
}
|
99
d2core/d2components/file_path.go
Normal file
99
d2core/d2components/file_path.go
Normal file
@ -0,0 +1,99 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
)
|
||||
|
||||
// static check that FilePathComponent implements Component
|
||||
var _ ecs.Component = &FilePathComponent{}
|
||||
|
||||
// static check that FilePathMap implements ComponentMap
|
||||
var _ ecs.ComponentMap = &FilePathMap{}
|
||||
|
||||
// FilePathComponent is a component that contains a file Path string
|
||||
type FilePathComponent struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FilePathComponent) ID() ecs.ComponentID {
|
||||
return FilePathCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FilePathComponent) NewMap() ecs.ComponentMap {
|
||||
return NewFilePathMap()
|
||||
}
|
||||
|
||||
// FilePath is a convenient reference to be used as a component identifier
|
||||
var FilePath = (*FilePathComponent)(nil) // nolint:gochecknoglobals // global by design
|
||||
|
||||
// NewFilePathMap creates a new map of entity ID's to FilePath
|
||||
func NewFilePathMap() *FilePathMap {
|
||||
cm := &FilePathMap{
|
||||
components: make(map[ecs.EID]*FilePathComponent),
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// FilePathMap is a map of entity ID's to FilePath
|
||||
type FilePathMap struct {
|
||||
world *ecs.World
|
||||
components map[ecs.EID]*FilePathComponent
|
||||
}
|
||||
|
||||
// Init initializes the component map with the given world
|
||||
func (cm *FilePathMap) Init(world *ecs.World) {
|
||||
cm.world = world
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FilePathMap) ID() ecs.ComponentID {
|
||||
return FilePathCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FilePathMap) NewMap() ecs.ComponentMap {
|
||||
return NewFilePathMap()
|
||||
}
|
||||
|
||||
// Add a new FilePathComponent for the given entity id, return that component.
|
||||
// If the entity already has a component, just return that one.
|
||||
func (cm *FilePathMap) Add(id ecs.EID) ecs.Component {
|
||||
if com, has := cm.components[id]; has {
|
||||
return com
|
||||
}
|
||||
|
||||
cm.components[id] = &FilePathComponent{Path: ""}
|
||||
|
||||
cm.world.UpdateEntity(id)
|
||||
|
||||
return cm.components[id]
|
||||
}
|
||||
|
||||
// AddFilePath adds a new FilePathComponent for the given entity id and returns it.
|
||||
// If the entity already has a FilePathComponent, just return that one.
|
||||
// this is a convenience method for the generic Add method, as it returns a
|
||||
// *FilePathComponent instead of an ecs.Component
|
||||
func (cm *FilePathMap) AddFilePath(id ecs.EID) *FilePathComponent {
|
||||
return cm.Add(id).(*FilePathComponent)
|
||||
}
|
||||
|
||||
// Get returns the component associated with the given entity id
|
||||
func (cm *FilePathMap) Get(id ecs.EID) (ecs.Component, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetFilePath returns the FilePathComponent associated with the given entity id
|
||||
func (cm *FilePathMap) GetFilePath(id ecs.EID) (*FilePathComponent, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// Remove a component for the given entity id, return the component.
|
||||
func (cm *FilePathMap) Remove(id ecs.EID) {
|
||||
delete(cm.components, id)
|
||||
cm.world.UpdateEntity(id)
|
||||
}
|
117
d2core/d2components/file_source.go
Normal file
117
d2core/d2components/file_source.go
Normal file
@ -0,0 +1,117 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||
)
|
||||
|
||||
// static check that FileSourceComponent implements Component
|
||||
var _ ecs.Component = &FileSourceComponent{}
|
||||
|
||||
// static check that FileSourceMap implements ComponentMap
|
||||
var _ ecs.ComponentMap = &FileSourceMap{}
|
||||
|
||||
// AbstractSource is the abstract representation of what a file source is
|
||||
type AbstractSource interface {
|
||||
Open(path *FilePathComponent) (d2interface.DataStream, error)
|
||||
}
|
||||
|
||||
// FileSourceComponent is a component that contains a FileSourceComponent instance
|
||||
type FileSourceComponent struct {
|
||||
AbstractSource
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FileSourceComponent) ID() ecs.ComponentID {
|
||||
return FileSourceCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FileSourceComponent) NewMap() ecs.ComponentMap {
|
||||
return NewFileSourceMap()
|
||||
}
|
||||
|
||||
// FileSource is a convenient reference to be used as a component identifier
|
||||
var FileSource = (*FileSourceComponent)(nil) // nolint:gochecknoglobals // global by design
|
||||
|
||||
// NewFileSourceMap creates a new map of entity ID's to FileSourceComponent components
|
||||
func NewFileSourceMap() *FileSourceMap {
|
||||
cm := &FileSourceMap{
|
||||
components: make(map[ecs.EID]*FileSourceComponent),
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// FileSourceMap is a map of entity ID's to FileSourceComponent type components
|
||||
type FileSourceMap struct {
|
||||
world *ecs.World
|
||||
components map[ecs.EID]*FileSourceComponent
|
||||
}
|
||||
|
||||
// Init initializes the component map with the given world
|
||||
func (cm *FileSourceMap) Init(world *ecs.World) {
|
||||
cm.world = world
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FileSourceMap) ID() ecs.ComponentID {
|
||||
return FileSourceCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FileSourceMap) NewMap() ecs.ComponentMap {
|
||||
return NewFileSourceMap()
|
||||
}
|
||||
|
||||
// Add a new FileSourceComponent for the given entity id, return that component.
|
||||
// If the entity already has a component, just return that one.
|
||||
func (cm *FileSourceMap) Add(id ecs.EID) ecs.Component {
|
||||
if com, has := cm.components[id]; has {
|
||||
return com
|
||||
}
|
||||
|
||||
cm.components[id] = &FileSourceComponent{}
|
||||
|
||||
cm.world.UpdateEntity(id)
|
||||
|
||||
return cm.components[id]
|
||||
}
|
||||
|
||||
// AddFileSource adds a new FileSourceComponent for the given entity id and returns it.
|
||||
// If the entity already has a file type component, just return that one.
|
||||
// this is a convenience method for the generic Add method, as it returns a
|
||||
// *FileSourceComponent instead of an ecs.Component
|
||||
func (cm *FileSourceMap) AddFileSource(id ecs.EID) *FileSourceComponent {
|
||||
return cm.Add(id).(*FileSourceComponent)
|
||||
}
|
||||
|
||||
// Get returns the component associated with the given entity id
|
||||
func (cm *FileSourceMap) Get(id ecs.EID) (ecs.Component, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetFileSource returns the FileSourceComponent type component associated with the given entity id
|
||||
func (cm *FileSourceMap) GetFileSource(id ecs.EID) (*FileSourceComponent, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetFileSources returns all FileSourceComponent components
|
||||
func (cm *FileSourceMap) GetFileSources() []*FileSourceComponent {
|
||||
result := make([]*FileSourceComponent, 0)
|
||||
|
||||
for _, src := range cm.components {
|
||||
result = append(result, src)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Remove a component for the given entity id, return the component.
|
||||
func (cm *FileSourceMap) Remove(id ecs.EID) {
|
||||
delete(cm.components, id)
|
||||
cm.world.UpdateEntity(id)
|
||||
}
|
101
d2core/d2components/file_type.go
Normal file
101
d2core/d2components/file_type.go
Normal file
@ -0,0 +1,101 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
)
|
||||
|
||||
// static check that FileTypeComponent implements Component
|
||||
var _ ecs.Component = &FileTypeComponent{}
|
||||
|
||||
// static check that FileTypeMap implements ComponentMap
|
||||
var _ ecs.ComponentMap = &FileTypeMap{}
|
||||
|
||||
// FileTypeComponent is a component that contains a file Type
|
||||
type FileTypeComponent struct {
|
||||
Type d2enum.FileType
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FileTypeComponent) ID() ecs.ComponentID {
|
||||
return FileTypeCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FileTypeComponent) NewMap() ecs.ComponentMap {
|
||||
return NewFileTypeMap()
|
||||
}
|
||||
|
||||
// FileType is a convenient reference to be used as a component identifier
|
||||
var FileType = (*FileTypeComponent)(nil) // nolint:gochecknoglobals // global by design
|
||||
|
||||
// NewFileTypeMap creates a new map of entity ID's to FileType
|
||||
func NewFileTypeMap() *FileTypeMap {
|
||||
cm := &FileTypeMap{
|
||||
components: make(map[ecs.EID]*FileTypeComponent),
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// FileTypeMap is a map of entity ID's to FileType
|
||||
type FileTypeMap struct {
|
||||
world *ecs.World
|
||||
components map[ecs.EID]*FileTypeComponent
|
||||
}
|
||||
|
||||
// Init initializes the component map with the given world
|
||||
func (cm *FileTypeMap) Init(world *ecs.World) {
|
||||
cm.world = world
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*FileTypeMap) ID() ecs.ComponentID {
|
||||
return FileTypeCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*FileTypeMap) NewMap() ecs.ComponentMap {
|
||||
return NewFileTypeMap()
|
||||
}
|
||||
|
||||
// Add a new FileTypeComponent for the given entity id, return that component.
|
||||
// If the entity already has a component, just return that one.
|
||||
func (cm *FileTypeMap) Add(id ecs.EID) ecs.Component {
|
||||
if com, has := cm.components[id]; has {
|
||||
return com
|
||||
}
|
||||
|
||||
cm.components[id] = &FileTypeComponent{Type: d2enum.FileTypeUnknown}
|
||||
|
||||
cm.world.UpdateEntity(id)
|
||||
|
||||
return cm.components[id]
|
||||
}
|
||||
|
||||
// AddFileType adds a new FileTypeComponent for the given entity id and returns it.
|
||||
// If the entity already has a file type component, just return that one.
|
||||
// this is a convenience method for the generic Add method, as it returns a
|
||||
// *FileTypeComponent instead of an ecs.Component
|
||||
func (cm *FileTypeMap) AddFileType(id ecs.EID) *FileTypeComponent {
|
||||
return cm.Add(id).(*FileTypeComponent)
|
||||
}
|
||||
|
||||
// Get returns the component associated with the given entity id
|
||||
func (cm *FileTypeMap) Get(id ecs.EID) (ecs.Component, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetFileType returns the FileTypeComponent associated with the given entity id
|
||||
func (cm *FileTypeMap) GetFileType(id ecs.EID) (*FileTypeComponent, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// Remove a component for the given entity id, return the component.
|
||||
func (cm *FileTypeMap) Remove(id ecs.EID) {
|
||||
delete(cm.components, id)
|
||||
cm.world.UpdateEntity(id)
|
||||
}
|
172
d2core/d2components/game_config.go
Normal file
172
d2core/d2components/game_config.go
Normal file
@ -0,0 +1,172 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
)
|
||||
|
||||
// static check that GameConfigComponent implements Component
|
||||
var _ ecs.Component = &GameConfigComponent{}
|
||||
|
||||
// static check that GameConfigMap implements ComponentMap
|
||||
var _ ecs.ComponentMap = &GameConfigMap{}
|
||||
|
||||
type GameConfigComponent struct {
|
||||
MpqLoadOrder []string
|
||||
Language string
|
||||
MpqPath string
|
||||
TicksPerSecond int
|
||||
FpsCap int
|
||||
SfxVolume float64
|
||||
BgmVolume float64
|
||||
FullScreen bool
|
||||
RunInBackground bool
|
||||
VsyncEnabled bool
|
||||
Backend string
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*GameConfigComponent) ID() ecs.ComponentID {
|
||||
return GameConfigCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*GameConfigComponent) NewMap() ecs.ComponentMap {
|
||||
return NewGameConfigMap()
|
||||
}
|
||||
|
||||
// GameConfig is a convenient reference to be used as a component identifier
|
||||
var GameConfig = (*GameConfigComponent)(nil) // nolint:gochecknoglobals // global by design
|
||||
|
||||
// NewGameConfigMap creates a new map of entity ID's to GameConfigComponent components
|
||||
func NewGameConfigMap() *GameConfigMap {
|
||||
cm := &GameConfigMap{
|
||||
components: make(map[ecs.EID]*GameConfigComponent),
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// GameConfigMap is a map of entity ID's to GameConfigComponent components
|
||||
type GameConfigMap struct {
|
||||
world *ecs.World
|
||||
components map[ecs.EID]*GameConfigComponent
|
||||
}
|
||||
|
||||
// Init initializes the component map with the given world
|
||||
func (cm *GameConfigMap) Init(world *ecs.World) {
|
||||
cm.world = world
|
||||
}
|
||||
|
||||
// Add a new GameConfigComponent for the given entity id, return that component.
|
||||
// If the entity already has a component, just return that one.
|
||||
func (cm *GameConfigMap) Add(id ecs.EID) ecs.Component {
|
||||
if com, has := cm.components[id]; has {
|
||||
return com
|
||||
}
|
||||
|
||||
cm.components[id] = defaultConfig()
|
||||
|
||||
cm.world.UpdateEntity(id)
|
||||
|
||||
return cm.components[id]
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*GameConfigMap) ID() ecs.ComponentID {
|
||||
return GameConfigCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*GameConfigMap) NewMap() ecs.ComponentMap {
|
||||
return NewGameConfigMap()
|
||||
}
|
||||
|
||||
// AddGameConfig adds a new GameConfigComponent for the given entity id and returns it.
|
||||
// If the entity already has a GameConfigComponent component, just return that one.
|
||||
// this is a convenience method for the generic Add method, as it returns a
|
||||
// *GameConfigComponent instead of an ecs.Component
|
||||
func (cm *GameConfigMap) AddGameConfig(id ecs.EID) *GameConfigComponent {
|
||||
return cm.Add(id).(*GameConfigComponent)
|
||||
}
|
||||
|
||||
// Get returns the component associated with the given entity id
|
||||
func (cm *GameConfigMap) Get(id ecs.EID) (ecs.Component, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetGameConfig returns the GameConfigComponent component associated with the given entity id
|
||||
func (cm *GameConfigMap) GetGameConfig(id ecs.EID) (*GameConfigComponent, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// Remove a component for the given entity id, return the component.
|
||||
func (cm *GameConfigMap) Remove(id ecs.EID) {
|
||||
delete(cm.components, id)
|
||||
cm.world.UpdateEntity(id)
|
||||
}
|
||||
|
||||
func defaultConfig() *GameConfigComponent {
|
||||
const (
|
||||
defaultSfxVolume = 1.0
|
||||
defaultBgmVolume = 0.3
|
||||
)
|
||||
|
||||
config := &GameConfigComponent{
|
||||
Language: "ENG",
|
||||
FullScreen: false,
|
||||
TicksPerSecond: -1,
|
||||
RunInBackground: true,
|
||||
VsyncEnabled: true,
|
||||
SfxVolume: defaultSfxVolume,
|
||||
BgmVolume: defaultBgmVolume,
|
||||
MpqPath: "C:/Program Files (x86)/Diablo II",
|
||||
Backend: "Ebiten",
|
||||
MpqLoadOrder: []string{
|
||||
"Patch_D2.mpq",
|
||||
"d2exp.mpq",
|
||||
"d2xmusic.mpq",
|
||||
"d2xtalk.mpq",
|
||||
"d2xvideo.mpq",
|
||||
"d2data.mpq",
|
||||
"d2char.mpq",
|
||||
"d2music.mpq",
|
||||
"d2sfx.mpq",
|
||||
"d2video.mpq",
|
||||
"d2speech.mpq",
|
||||
},
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if runtime.GOARCH == "386" {
|
||||
config.MpqPath = "C:/Program Files/Diablo II"
|
||||
}
|
||||
case "darwin":
|
||||
config.MpqPath = "/Applications/Diablo II/"
|
||||
config.MpqLoadOrder = []string{
|
||||
"Diablo II Patch",
|
||||
"Diablo II Expansion Data",
|
||||
"Diablo II Expansion Movies",
|
||||
"Diablo II Expansion Music",
|
||||
"Diablo II Expansion Speech",
|
||||
"Diablo II Game Data",
|
||||
"Diablo II Graphics",
|
||||
"Diablo II Movies",
|
||||
"Diablo II Music",
|
||||
"Diablo II Sounds",
|
||||
"Diablo II Speech",
|
||||
}
|
||||
case "linux":
|
||||
if usr, err := user.Current(); err == nil {
|
||||
config.MpqPath = path.Join(usr.HomeDir, ".wine/drive_c/Program Files (x86)/Diablo II")
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
102
d2core/d2components/position.go
Normal file
102
d2core/d2components/position.go
Normal file
@ -0,0 +1,102 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
|
||||
)
|
||||
|
||||
// static check that PositionComponent implements Component
|
||||
var _ ecs.Component = &PositionComponent{}
|
||||
|
||||
// static check that PositionMap implements ComponentMap
|
||||
var _ ecs.ComponentMap = &PositionMap{}
|
||||
|
||||
// PositionComponent stores an x,y position
|
||||
type PositionComponent struct {
|
||||
*d2vector.Position
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*PositionComponent) ID() ecs.ComponentID {
|
||||
return PositionCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*PositionComponent) NewMap() ecs.ComponentMap {
|
||||
return NewPositionMap()
|
||||
}
|
||||
|
||||
// Position is a convenient reference to be used as a component identifier
|
||||
var Position = (*PositionComponent)(nil) // nolint:gochecknoglobals // global by design
|
||||
|
||||
// NewPositionMap creates a new map of entity ID's to position components
|
||||
func NewPositionMap() *PositionMap {
|
||||
cm := &PositionMap{
|
||||
components: make(map[ecs.EID]*PositionComponent),
|
||||
}
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// PositionMap is a map of entity ID's to position components
|
||||
type PositionMap struct {
|
||||
world *ecs.World
|
||||
components map[ecs.EID]*PositionComponent
|
||||
}
|
||||
|
||||
// Init initializes the component map with the given world
|
||||
func (cm *PositionMap) Init(world *ecs.World) {
|
||||
cm.world = world
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*PositionMap) ID() ecs.ComponentID {
|
||||
return PositionCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*PositionMap) NewMap() ecs.ComponentMap {
|
||||
return NewPositionMap()
|
||||
}
|
||||
|
||||
// Add a new PositionComponent for the given entity id, return that component.
|
||||
// If the entity already has a component, just return that one.
|
||||
func (cm *PositionMap) Add(id ecs.EID) ecs.Component {
|
||||
if com, has := cm.components[id]; has {
|
||||
return com
|
||||
}
|
||||
|
||||
position := d2vector.NewPosition(0, 0)
|
||||
cm.components[id] = &PositionComponent{Position: &position}
|
||||
|
||||
cm.world.UpdateEntity(id)
|
||||
|
||||
return cm.components[id]
|
||||
}
|
||||
|
||||
// AddPosition adds a new PositionComponent for the given entity id and returns it.
|
||||
// If the entity already has a position component, just return that one.
|
||||
// this is a convenience method for the generic Add method, as it returns a
|
||||
// *PositionComponent instead of an ecs.Component
|
||||
func (cm *PositionMap) AddPosition(id ecs.EID) *PositionComponent {
|
||||
return cm.Add(id).(*PositionComponent)
|
||||
}
|
||||
|
||||
// Get returns the component associated with the given entity id
|
||||
func (cm *PositionMap) Get(id ecs.EID) (ecs.Component, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetPosition returns the position component associated with the given entity id
|
||||
func (cm *PositionMap) GetPosition(id ecs.EID) (*PositionComponent, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// Remove a component for the given entity id, return the component.
|
||||
func (cm *PositionMap) Remove(id ecs.EID) {
|
||||
delete(cm.components, id)
|
||||
cm.world.UpdateEntity(id)
|
||||
}
|
102
d2core/d2components/velocity.go
Normal file
102
d2core/d2components/velocity.go
Normal file
@ -0,0 +1,102 @@
|
||||
package d2components
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
|
||||
)
|
||||
|
||||
// static check that VelocityComponent implements Component
|
||||
var _ ecs.Component = &VelocityComponent{}
|
||||
|
||||
// static check that VelocityMap implements ComponentMap
|
||||
var _ ecs.ComponentMap = &VelocityMap{}
|
||||
|
||||
// VelocityComponent stores the velocity as a vec2
|
||||
type VelocityComponent struct {
|
||||
*d2vector.Vector
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*VelocityComponent) ID() ecs.ComponentID {
|
||||
return VelocityCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*VelocityComponent) NewMap() ecs.ComponentMap {
|
||||
return NewVelocityMap()
|
||||
}
|
||||
|
||||
// Velocity is a convenient reference to be used as a component identifier
|
||||
var Velocity = (*VelocityComponent)(nil) // nolint:gochecknoglobals // global by design
|
||||
|
||||
// NewVelocityMap creates a new map of entity ID's to velocity components
|
||||
func NewVelocityMap() *VelocityMap {
|
||||
return &VelocityMap{
|
||||
components: make(map[ecs.EID]*VelocityComponent),
|
||||
}
|
||||
}
|
||||
|
||||
// VelocityMap is a map of entity ID's to velocity components
|
||||
type VelocityMap struct {
|
||||
world *ecs.World
|
||||
components map[ecs.EID]*VelocityComponent
|
||||
}
|
||||
|
||||
// Init initializes the component map with the given world
|
||||
func (cm *VelocityMap) Init(world *ecs.World) {
|
||||
cm.world = world
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for the component type
|
||||
func (*VelocityMap) ID() ecs.ComponentID {
|
||||
return VelocityCID
|
||||
}
|
||||
|
||||
// NewMap returns a new component map the component type
|
||||
func (*VelocityMap) NewMap() ecs.ComponentMap {
|
||||
return NewVelocityMap()
|
||||
}
|
||||
|
||||
// Add a new VelocityComponent for the given entity id, return that component.
|
||||
// If the entity already has a component, just return that one.
|
||||
func (cm *VelocityMap) Add(id ecs.EID) ecs.Component {
|
||||
if com, has := cm.components[id]; has {
|
||||
return com
|
||||
}
|
||||
|
||||
v := d2vector.NewVector(0, 0)
|
||||
com := &VelocityComponent{Vector: v}
|
||||
cm.components[id] = com
|
||||
|
||||
cm.world.UpdateEntity(id)
|
||||
|
||||
return com
|
||||
}
|
||||
|
||||
// AddVelocity adds a new VelocityComponent for the given entity id and returns it.
|
||||
// If the entity already has a component, just return that one.
|
||||
// this is a convenience method for the generic Add method, as it returns a
|
||||
// *VelocityComponent instead of an ecs.Component
|
||||
func (cm *VelocityMap) AddVelocity(id ecs.EID) *VelocityComponent {
|
||||
return cm.Add(id).(*VelocityComponent)
|
||||
}
|
||||
|
||||
// Get returns the component associated with the given entity id
|
||||
func (cm *VelocityMap) Get(id ecs.EID) (ecs.Component, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// GetVelocity returns the velocity component associated with the given entity id.
|
||||
// This is used to return a *VelocityComponent, as opposed to an ecs.Component
|
||||
func (cm *VelocityMap) GetVelocity(id ecs.EID) (*VelocityComponent, bool) {
|
||||
entry, found := cm.components[id]
|
||||
return entry, found
|
||||
}
|
||||
|
||||
// Remove a component for the given entity id, return the component.
|
||||
func (cm *VelocityMap) Remove(id ecs.EID) {
|
||||
delete(cm.components, id)
|
||||
cm.world.UpdateEntity(id)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package d2records
|
||||
|
||||
// ComponentCodes is a lookup table for DCC Animation Component Subtype,
|
||||
// ComponentCodes is a lookup table for DCC Animation ComponentID Subtype,
|
||||
// it links hardcoded data with the txt files
|
||||
type ComponentCodes map[string]*ComponentCodeRecord
|
||||
|
||||
|
204
d2core/d2systems/asset_loader.go
Normal file
204
d2core/d2systems/asset_loader.go
Normal file
@ -0,0 +1,204 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
func NewAssetLoader() *AssetLoaderSystem {
|
||||
filesToLoad := ecs.NewFilter()
|
||||
|
||||
// subscribe to entities with a file path+type+handle, ready to be loaded
|
||||
filesToLoad.Require(d2components.FilePath, d2components.FileType, d2components.FileHandle)
|
||||
|
||||
// exclude entities that have already been loaded
|
||||
filesToLoad.Forbid(d2components.GameConfig).
|
||||
//Forbid(d2components.AssetStringTableCID).
|
||||
//Forbid(d2components.AssetDataDictionaryCID).
|
||||
//Forbid(d2components.AssetPaletteCID).
|
||||
//Forbid(d2components.AssetPaletteTransformCID).
|
||||
//Forbid(d2components.AssetCofCID).
|
||||
//Forbid(d2components.AssetDc6CID).
|
||||
//Forbid(d2components.AssetDccCID).
|
||||
//Forbid(d2components.AssetDs1CID).
|
||||
//Forbid(d2components.AssetDt1CID).
|
||||
//Forbid(d2components.AssetWavCID).
|
||||
//Forbid(d2components.AssetD2CID).
|
||||
Build()
|
||||
|
||||
// subscribe to entities that have a source type and a source component
|
||||
fileSources := ecs.NewFilter().
|
||||
Require(d2components.FileSource).
|
||||
Build()
|
||||
|
||||
return &AssetLoaderSystem{
|
||||
SubscriberSystem: ecs.NewSubscriberSystem(filesToLoad.Build(), fileSources),
|
||||
}
|
||||
}
|
||||
|
||||
var _ ecs.System = &AssetLoaderSystem{}
|
||||
|
||||
type AssetLoaderSystem struct {
|
||||
*ecs.SubscriberSystem
|
||||
fileSub *ecs.Subscription
|
||||
sourceSub *ecs.Subscription
|
||||
filePaths *d2components.FilePathMap
|
||||
fileTypes *d2components.FileTypeMap
|
||||
fileHandles *d2components.FileHandleMap
|
||||
fileSources *d2components.FileSourceMap
|
||||
}
|
||||
|
||||
// Init initializes the system with the given world
|
||||
func (m *AssetLoaderSystem) Init(world *ecs.World) {
|
||||
m.World = world
|
||||
|
||||
if world == nil {
|
||||
m.SetActive(false)
|
||||
return
|
||||
}
|
||||
|
||||
for subIdx := range m.Subscriptions {
|
||||
m.AddSubscription(m.Subscriptions[subIdx])
|
||||
}
|
||||
|
||||
m.fileSub = m.Subscriptions[0]
|
||||
m.sourceSub = m.Subscriptions[1]
|
||||
|
||||
// try to inject the components we require, then cast the returned
|
||||
// abstract ComponentMap back to the concrete implementation
|
||||
m.filePaths = m.InjectMap(d2components.FilePath).(*d2components.FilePathMap)
|
||||
m.fileTypes = m.InjectMap(d2components.FileType).(*d2components.FileTypeMap)
|
||||
m.fileHandles = m.InjectMap(d2components.FileHandle).(*d2components.FileHandleMap)
|
||||
m.fileSources = m.InjectMap(d2components.FileSource).(*d2components.FileSourceMap)
|
||||
}
|
||||
|
||||
// Process processes all of the Entities
|
||||
func (m *AssetLoaderSystem) Process() {
|
||||
for _, eid := range m.fileSub.GetEntities() {
|
||||
m.ProcessEntity(eid)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessEntity updates an individual entity in the system
|
||||
func (m *AssetLoaderSystem) ProcessEntity(id ecs.EID) {
|
||||
ft, found := m.fileTypes.GetFileType(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
fh, found := m.fileHandles.GetFileHandle(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
data, buf := make([]byte, 0), make([]byte, 16)
|
||||
for {
|
||||
numRead, err := fh.Data.Read(buf)
|
||||
data = append(data, buf[:numRead]...)
|
||||
if numRead < 1 || err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch ft.Type {
|
||||
case d2enum.FileTypeJSON:
|
||||
err = m.loadFileTypeJSON(id, data)
|
||||
//case d2enum.FileTypeStringTable:
|
||||
// err = m.loadFileTypeStringTable(id, data)
|
||||
//case d2enum.FileTypeDataDictionary:
|
||||
// err = m.loadFileTypeDataDictionary(id, data)
|
||||
//case d2enum.FileTypePalette:
|
||||
// err = m.loadFileTypePalette(id, data)
|
||||
//case d2enum.FileTypePaletteTransform:
|
||||
// err = m.loadFileTypePaletteTransform(id, data)
|
||||
//case d2enum.FileTypeCOF:
|
||||
// err = m.loadFileTypeCOF(id, data)
|
||||
//case d2enum.FileTypeDC6:
|
||||
// err = m.loadFileTypeDC6(id, data)
|
||||
//case d2enum.FileTypeDCC:
|
||||
// err = m.loadFileTypeDCC(id, data)
|
||||
//case d2enum.FileTypeDS1:
|
||||
// err = m.loadFileTypeDS1(id, data)
|
||||
//case d2enum.FileTypeDT1:
|
||||
// err = m.loadFileTypeDT1(id, data)
|
||||
//case d2enum.FileTypeWAV:
|
||||
// err = m.loadFileTypeWAV(id, data)
|
||||
//case d2enum.FileTypeD2:
|
||||
// err = m.loadFileTypeD2(id, data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ft.Type = d2enum.FileTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AssetLoaderSystem) loadFileTypeJSON(id ecs.EID, data []byte) error {
|
||||
_, found := m.filePaths.GetFilePath(id)
|
||||
if !found {
|
||||
return errors.New("file path component for entity not found")
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
|
||||
// Unmarshal or Decode the JSON to the interface.
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mpq := result["MpqLoadOrder"].([]interface{})
|
||||
fmt.Println("Address :", mpq)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (m *AssetLoaderSystem) loadFileTypeStringTable(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeDataDictionary(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypePalette(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypePaletteTransform(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeCOF(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeDC6(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeDCC(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeDS1(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeDT1(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeWAV(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (m *AssetLoaderSystem) loadFileTypeD2(id ecs.EID, data []byte) error {
|
||||
//
|
||||
//}
|
78
d2core/d2systems/file_handle_resolver.go
Normal file
78
d2core/d2systems/file_handle_resolver.go
Normal file
@ -0,0 +1,78 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
func NewFileHandleResolver() *FileHandleResolutionSystem {
|
||||
// this filter is for entities that have a file path and file type but no file handle.
|
||||
filesToSource := ecs.NewFilter().
|
||||
Require(d2components.FilePath, d2components.FileType).
|
||||
Forbid(d2components.FileHandle, d2components.FileSource).
|
||||
Build()
|
||||
|
||||
return &FileHandleResolutionSystem{
|
||||
SubscriberSystem: ecs.NewSubscriberSystem(filesToSource),
|
||||
}
|
||||
}
|
||||
|
||||
type FileHandleResolutionSystem struct {
|
||||
*ecs.SubscriberSystem
|
||||
fileSub *ecs.Subscription
|
||||
filePaths *d2components.FilePathMap
|
||||
fileTypes *d2components.FileTypeMap
|
||||
fileSources *d2components.FileSourceMap
|
||||
fileHandles *d2components.FileHandleMap
|
||||
}
|
||||
|
||||
// Init initializes the system with the given world
|
||||
func (m *FileHandleResolutionSystem) Init(world *ecs.World) {
|
||||
m.World = world
|
||||
|
||||
for subIdx := range m.Subscriptions {
|
||||
m.AddSubscription(m.Subscriptions[subIdx])
|
||||
}
|
||||
|
||||
if world == nil {
|
||||
m.SetActive(false)
|
||||
return
|
||||
}
|
||||
|
||||
m.fileSub = m.Subscriptions[0]
|
||||
|
||||
// try to inject the components we require, then cast the returned
|
||||
// abstract ComponentMap back to the concrete implementation
|
||||
m.filePaths = m.InjectMap(d2components.FilePath).(*d2components.FilePathMap)
|
||||
m.fileTypes = m.InjectMap(d2components.FileType).(*d2components.FileTypeMap)
|
||||
m.fileHandles = m.InjectMap(d2components.FileHandle).(*d2components.FileHandleMap)
|
||||
m.fileSources = m.InjectMap(d2components.FileSource).(*d2components.FileSourceMap)
|
||||
}
|
||||
|
||||
// Process processes all of the Entities
|
||||
func (m *FileHandleResolutionSystem) Process() {
|
||||
for _, EID := range m.fileSub.GetEntities() {
|
||||
m.ProcessEntity(EID)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessEntity updates an individual entity in the system
|
||||
func (m *FileHandleResolutionSystem) ProcessEntity(id ecs.EID) {
|
||||
fp, found := m.filePaths.GetFilePath(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
for _, source := range m.fileSources.GetFileSources() {
|
||||
data, err := source.Open(fp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dataComponent := m.fileHandles.AddFileHandle(id)
|
||||
dataComponent.Data = data
|
||||
|
||||
break
|
||||
}
|
||||
}
|
87
d2core/d2systems/file_handle_resolver_test.go
Normal file
87
d2core/d2systems/file_handle_resolver_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
func Test_FileHandleResolver_Process(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
fileTypeResolver := NewFileTypeResolver()
|
||||
fileHandleResolver := NewFileHandleResolver()
|
||||
fileSourceResolver := NewFileSourceResolver()
|
||||
|
||||
cfg.With(fileTypeResolver).
|
||||
With(fileSourceResolver).
|
||||
With(fileHandleResolver)
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
filepathMap, err := world.GetMap(d2components.FilePath)
|
||||
if err != nil {
|
||||
t.Error("file path component map not found")
|
||||
}
|
||||
|
||||
filePaths := filepathMap.(*d2components.FilePathMap)
|
||||
|
||||
sourceEntity := world.NewEntity()
|
||||
sourceFp := filePaths.AddFilePath(sourceEntity)
|
||||
sourceFp.Path = "./testdata/"
|
||||
|
||||
//_ = world.Update(0)
|
||||
|
||||
fileEntity := world.NewEntity()
|
||||
fileFp := filePaths.AddFilePath(fileEntity)
|
||||
fileFp.Path = "testfile_a.txt"
|
||||
|
||||
_ = world.Update(0)
|
||||
|
||||
ft, found := fileTypeResolver.fileTypes.GetFileType(sourceEntity)
|
||||
if !found {
|
||||
t.Error("file source type not created for entity")
|
||||
return
|
||||
}
|
||||
|
||||
if ft.Type != d2enum.FileTypeDirectory {
|
||||
t.Error("expected file system source type for entity")
|
||||
return
|
||||
}
|
||||
|
||||
handleMap, err := world.GetMap(d2components.FileHandle)
|
||||
if err != nil {
|
||||
t.Error("file handle component map is nil")
|
||||
return
|
||||
}
|
||||
|
||||
fileHandles := handleMap.(*d2components.FileHandleMap)
|
||||
|
||||
handle, found := fileHandles.GetFileHandle(fileEntity)
|
||||
if !found {
|
||||
t.Error("file handle for entity was not found")
|
||||
return
|
||||
}
|
||||
|
||||
data, buf := make([]byte, 0), make([]byte, 16)
|
||||
|
||||
for {
|
||||
numRead, err := handle.Data.Read(buf)
|
||||
|
||||
data = append(data, buf[:numRead]...)
|
||||
|
||||
if err != nil || numRead == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result := strings.Trim(string(data), "\r\n")
|
||||
|
||||
if result != "test a" {
|
||||
t.Error("unexpected data read from `./testdata/testfile_a.txt`")
|
||||
}
|
||||
}
|
150
d2core/d2systems/file_source_resolver.go
Normal file
150
d2core/d2systems/file_source_resolver.go
Normal file
@ -0,0 +1,150 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2mpq"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
func NewFileSourceResolver() *FileSourceResolver {
|
||||
// subscribe to entities with a file type and file path, but no file source type
|
||||
filesToCheck := ecs.NewFilter().
|
||||
Require(d2components.FilePath).
|
||||
Require(d2components.FileType).
|
||||
Forbid(d2components.FileSource).
|
||||
Build()
|
||||
|
||||
return &FileSourceResolver{
|
||||
SubscriberSystem: ecs.NewSubscriberSystem(filesToCheck),
|
||||
}
|
||||
}
|
||||
|
||||
type FileSourceResolver struct {
|
||||
*ecs.SubscriberSystem
|
||||
fileSub *ecs.Subscription
|
||||
filePaths *d2components.FilePathMap
|
||||
fileTypes *d2components.FileTypeMap
|
||||
fileSources *d2components.FileSourceMap
|
||||
}
|
||||
|
||||
// Init initializes the system with the given world
|
||||
func (m *FileSourceResolver) Init(world *ecs.World) {
|
||||
m.World = world
|
||||
|
||||
if world == nil {
|
||||
m.SetActive(false)
|
||||
return
|
||||
}
|
||||
|
||||
for subIdx := range m.Subscriptions {
|
||||
m.AddSubscription(m.Subscriptions[subIdx])
|
||||
}
|
||||
|
||||
m.fileSub = m.Subscriptions[0]
|
||||
|
||||
// try to inject the components we require, then cast the returned
|
||||
// abstract ComponentMap back to the concrete implementation
|
||||
m.filePaths = m.InjectMap(d2components.FilePath).(*d2components.FilePathMap)
|
||||
m.fileTypes = m.InjectMap(d2components.FileType).(*d2components.FileTypeMap)
|
||||
m.fileSources = m.InjectMap(d2components.FileSource).(*d2components.FileSourceMap)
|
||||
}
|
||||
|
||||
// Process processes all of the Entities
|
||||
func (m *FileSourceResolver) Process() {
|
||||
for subIdx := range m.Subscriptions {
|
||||
for _, sourceEntityID := range m.Subscriptions[subIdx].GetEntities() {
|
||||
m.ProcessEntity(sourceEntityID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessEntity updates an individual entity in the system
|
||||
func (m *FileSourceResolver) ProcessEntity(id ecs.EID) {
|
||||
fp, found := m.filePaths.GetFilePath(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
fst, found := m.fileTypes.GetFileType(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
switch fst.Type {
|
||||
case d2enum.FileTypeUnknown:
|
||||
return
|
||||
case d2enum.FileTypeMPQ:
|
||||
source := m.fileSources.AddFileSource(id)
|
||||
instance, err := m.makeMpqSource(fp.Path)
|
||||
|
||||
if err != nil {
|
||||
fst.Type = d2enum.FileTypeUnknown
|
||||
break
|
||||
}
|
||||
|
||||
source.AbstractSource = instance
|
||||
case d2enum.FileTypeDirectory:
|
||||
source := m.fileSources.AddFileSource(id)
|
||||
instance, err := m.makeFileSystemSource(fp.Path)
|
||||
|
||||
if err != nil {
|
||||
fst.Type = d2enum.FileTypeUnknown
|
||||
break
|
||||
}
|
||||
|
||||
source.AbstractSource = instance
|
||||
default:
|
||||
fst.Type = d2enum.FileTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// filesystem source
|
||||
func (m *FileSourceResolver) makeFileSystemSource(path string) (d2components.AbstractSource, error) {
|
||||
return &fsSource{rootDir: path}, nil
|
||||
}
|
||||
|
||||
type fsSource struct {
|
||||
rootDir string
|
||||
}
|
||||
|
||||
func (s *fsSource) Open(path *d2components.FilePathComponent) (d2interface.DataStream, error) {
|
||||
fileData, err := os.Open(s.fullPath(path.Path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileData, nil
|
||||
}
|
||||
|
||||
func (s *fsSource) fullPath(path string) string {
|
||||
return filepath.Clean(filepath.Join(s.rootDir, path))
|
||||
}
|
||||
|
||||
// mpq source
|
||||
func (m *FileSourceResolver) makeMpqSource(path string) (d2components.AbstractSource, error) {
|
||||
mpq, err := d2mpq.Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mpqSource{mpq: mpq}, nil
|
||||
}
|
||||
|
||||
type mpqSource struct {
|
||||
mpq d2interface.Archive
|
||||
}
|
||||
|
||||
func (s *mpqSource) Open(path *d2components.FilePathComponent) (d2interface.DataStream, error) {
|
||||
fileData, err := s.mpq.ReadFileStream(path.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileData, nil
|
||||
}
|
44
d2core/d2systems/file_source_resolver_test.go
Normal file
44
d2core/d2systems/file_source_resolver_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
func Test_FileSourceResolution(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
srcResolver := NewFileSourceResolver()
|
||||
fileTypeResolver := NewFileTypeResolver()
|
||||
|
||||
cfg.With(fileTypeResolver).
|
||||
With(srcResolver)
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
filepathMap, err := world.GetMap(d2components.FilePath)
|
||||
if err != nil {
|
||||
t.Error("file path component map not found")
|
||||
}
|
||||
|
||||
filePaths := filepathMap.(*d2components.FilePathMap)
|
||||
|
||||
sourceEntity := world.NewEntity()
|
||||
sourceFp := filePaths.AddFilePath(sourceEntity)
|
||||
sourceFp.Path = "./testdata/"
|
||||
|
||||
_ = world.Update(0)
|
||||
|
||||
ft, found := fileTypeResolver.fileTypes.GetFileType(sourceEntity)
|
||||
if !found {
|
||||
t.Error("file source type not created for entity")
|
||||
}
|
||||
|
||||
if ft.Type != d2enum.FileTypeDirectory {
|
||||
t.Error("expected file system source type for entity")
|
||||
}
|
||||
}
|
128
d2core/d2systems/file_type_resolver.go
Normal file
128
d2core/d2systems/file_type_resolver.go
Normal file
@ -0,0 +1,128 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2mpq"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
// NewFileTypeResolver creates a new file type resolution system.
|
||||
func NewFileTypeResolver() *FileTypeResolutionSystem {
|
||||
cfg := ecs.NewFilter()
|
||||
|
||||
cfg.Require(d2components.FilePath)
|
||||
|
||||
filter := cfg.Build()
|
||||
|
||||
return &FileTypeResolutionSystem{
|
||||
SubscriberSystem: ecs.NewSubscriberSystem(filter),
|
||||
}
|
||||
}
|
||||
|
||||
// static check that FileTypeResolutionSystem implements the System interface
|
||||
var _ ecs.System = &FileTypeResolutionSystem{}
|
||||
|
||||
// FileTypeResolutionSystem is responsible for determining file types from file file paths.
|
||||
// This system will subscribe to entities that have a file path component, but do not
|
||||
// have a file type component. It will use the file path component to determine the file type,
|
||||
// and it will then create the file type component for the entity, thus removing the entity
|
||||
// from its subscription.
|
||||
type FileTypeResolutionSystem struct {
|
||||
*ecs.SubscriberSystem
|
||||
filePaths *d2components.FilePathMap
|
||||
fileTypes *d2components.FileTypeMap
|
||||
}
|
||||
|
||||
// Init initializes the system with the given world
|
||||
func (m *FileTypeResolutionSystem) Init(world *ecs.World) {
|
||||
m.World = world
|
||||
|
||||
if world == nil {
|
||||
m.SetActive(false)
|
||||
return
|
||||
}
|
||||
|
||||
for subIdx := range m.Subscriptions {
|
||||
m.AddSubscription(m.Subscriptions[subIdx])
|
||||
}
|
||||
|
||||
// try to inject the components we require, then cast the returned
|
||||
// abstract ComponentMap back to the concrete implementation
|
||||
m.filePaths = m.InjectMap(d2components.FilePath).(*d2components.FilePathMap)
|
||||
m.fileTypes = m.InjectMap(d2components.FileType).(*d2components.FileTypeMap)
|
||||
}
|
||||
|
||||
// Process processes all of the Entities
|
||||
func (m *FileTypeResolutionSystem) Process() {
|
||||
for subIdx := range m.Subscriptions {
|
||||
entities := m.Subscriptions[subIdx].GetEntities()
|
||||
for entIdx := range entities {
|
||||
m.ProcessEntity(entities[entIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessEntity updates an individual entity in the system
|
||||
func (m *FileTypeResolutionSystem) ProcessEntity(id ecs.EID) {
|
||||
fp, found := m.filePaths.GetFilePath(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
ft := m.fileTypes.AddFileType(id)
|
||||
|
||||
if _, err := d2mpq.Load(fp.Path); err == nil {
|
||||
ft.Type = d2enum.FileTypeMPQ
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(fp.Path))
|
||||
|
||||
switch ext {
|
||||
case ".mpq":
|
||||
ft.Type = d2enum.FileTypeMPQ
|
||||
case ".d2":
|
||||
ft.Type = d2enum.FileTypeD2
|
||||
case ".dcc":
|
||||
ft.Type = d2enum.FileTypeDCC
|
||||
case ".dc6":
|
||||
ft.Type = d2enum.FileTypeDC6
|
||||
case ".wav":
|
||||
ft.Type = d2enum.FileTypeWAV
|
||||
case ".ds1":
|
||||
ft.Type = d2enum.FileTypeDS1
|
||||
case ".dt1":
|
||||
ft.Type = d2enum.FileTypeDT1
|
||||
case ".pl2":
|
||||
ft.Type = d2enum.FileTypePaletteTransform
|
||||
case ".dat":
|
||||
ft.Type = d2enum.FileTypePalette
|
||||
case ".tbl":
|
||||
ft.Type = d2enum.FileTypeStringTable
|
||||
case ".txt":
|
||||
ft.Type = d2enum.FileTypeDataDictionary
|
||||
case ".cof":
|
||||
ft.Type = d2enum.FileTypeCOF
|
||||
case ".json":
|
||||
ft.Type = d2enum.FileTypeJSON
|
||||
default:
|
||||
cleanPath := filepath.Clean(fp.Path)
|
||||
|
||||
info, err := os.Lstat(cleanPath)
|
||||
if err != nil {
|
||||
ft.Type = d2enum.FileTypeUnknown
|
||||
return
|
||||
}
|
||||
|
||||
if info.Mode().IsDir() {
|
||||
ft.Type = d2enum.FileTypeDirectory
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
66
d2core/d2systems/file_type_resolver_test.go
Normal file
66
d2core/d2systems/file_type_resolver_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
)
|
||||
|
||||
func TestNewFileTypeResolver_KnownType(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
resolver := NewFileTypeResolver()
|
||||
|
||||
cfg.With(resolver)
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
e := world.NewEntity()
|
||||
|
||||
fp := resolver.filePaths.AddFilePath(e)
|
||||
fp.Path = "/some/path/to/a/file.dcc"
|
||||
|
||||
if len(resolver.Subscriptions[0].GetEntities()) != 1 {
|
||||
t.Error("entity with file path not added to file type resolver subscription")
|
||||
}
|
||||
|
||||
_ = world.Update(0)
|
||||
|
||||
if len(resolver.Subscriptions[0].GetEntities()) != 0 {
|
||||
t.Error("entity with existing file type not removed from file type resolver subscription")
|
||||
}
|
||||
|
||||
ft, found := resolver.fileTypes.GetFileType(e)
|
||||
if !found {
|
||||
t.Error("file type component not added to entity with file path component")
|
||||
}
|
||||
|
||||
if ft.Type != d2enum.FileTypeDCC {
|
||||
t.Error("unexpected file type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFileTypeResolver_UnknownType(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
resolver := NewFileTypeResolver()
|
||||
|
||||
cfg.With(resolver)
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
e := world.NewEntity()
|
||||
|
||||
fp := resolver.filePaths.AddFilePath(e)
|
||||
fp.Path = "/some/path/to/a/file.XYZ"
|
||||
|
||||
_ = world.Update(0)
|
||||
|
||||
ft, _ := resolver.fileTypes.GetFileType(e)
|
||||
|
||||
if ft.Type != d2enum.FileTypeUnknown {
|
||||
t.Error("unexpected file type")
|
||||
}
|
||||
}
|
59
d2core/d2systems/game_config.go
Normal file
59
d2core/d2systems/game_config.go
Normal file
@ -0,0 +1,59 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
// static check that the game config system implements the system interface
|
||||
var _ ecs.System = &GameConfigSystem{}
|
||||
|
||||
func NewGameConfigSystem() *GameConfigSystem {
|
||||
gameConfigs := ecs.NewFilter().
|
||||
Require(d2components.GameConfig).
|
||||
Build()
|
||||
|
||||
gcs := &GameConfigSystem{
|
||||
SubscriberSystem: ecs.NewSubscriberSystem(gameConfigs),
|
||||
}
|
||||
|
||||
return gcs
|
||||
}
|
||||
|
||||
type GameConfigSystem struct {
|
||||
*ecs.SubscriberSystem
|
||||
configs *d2components.GameConfigMap
|
||||
filePaths *d2components.FilePathMap
|
||||
fileTypes *d2components.FileTypeMap
|
||||
fileHandles *d2components.FileHandleMap
|
||||
fileSources *d2components.FileSourceMap
|
||||
}
|
||||
|
||||
func (m *GameConfigSystem) Init(world *ecs.World) {
|
||||
m.World = world
|
||||
|
||||
if world == nil {
|
||||
m.SetActive(false)
|
||||
return
|
||||
}
|
||||
|
||||
for subIdx := range m.Subscriptions {
|
||||
m.AddSubscription(m.Subscriptions[subIdx])
|
||||
}
|
||||
|
||||
// try to inject the components we require, then cast the returned
|
||||
// abstract ComponentMap back to the concrete implementation
|
||||
m.filePaths = world.InjectMap(d2components.FilePath).(*d2components.FilePathMap)
|
||||
m.fileTypes = world.InjectMap(d2components.FileType).(*d2components.FileTypeMap)
|
||||
m.fileHandles = world.InjectMap(d2components.FileHandle).(*d2components.FileHandleMap)
|
||||
m.fileSources = world.InjectMap(d2components.FileSource).(*d2components.FileSourceMap)
|
||||
}
|
||||
|
||||
func (m *GameConfigSystem) Process() {
|
||||
for subIdx := range m.Subscriptions {
|
||||
for _, EID := range m.Subscriptions[subIdx].GetEntities() {
|
||||
_ = EID
|
||||
}
|
||||
}
|
||||
}
|
57
d2core/d2systems/integration_test.go
Normal file
57
d2core/d2systems/integration_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
)
|
||||
|
||||
func Test_SystemIntegrationTest(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
scale := NewTimeScaleSystem()
|
||||
movement := NewMovementSystem()
|
||||
|
||||
cfg.With(scale)
|
||||
cfg.With(movement)
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
e := world.NewEntity()
|
||||
pos := movement.positions.AddPosition(e)
|
||||
vel := movement.velocities.AddVelocity(e)
|
||||
|
||||
vel.Set(1, 2)
|
||||
|
||||
// first test without time scaling active
|
||||
scale.scale = 0.001
|
||||
scale.SetActive(false)
|
||||
|
||||
timeDelta := time.Millisecond
|
||||
|
||||
expectX, expectY := pos.X()+vel.X(), pos.Y()+vel.Y()
|
||||
|
||||
for idx := 0; idx < 1000; idx++ {
|
||||
_ = world.Update(timeDelta)
|
||||
}
|
||||
|
||||
if !pos.EqualsApprox(vel.Vector) {
|
||||
fmtStr := "position component not updated, expected (%v,%v) but got (%v,%v)"
|
||||
t.Errorf(fmtStr, expectX, expectY, pos.X(), pos.Y())
|
||||
}
|
||||
|
||||
// now enable time scaling
|
||||
scale.SetActive(true)
|
||||
|
||||
expectX, expectY = pos.X()+vel.X(), pos.Y()+vel.Y()
|
||||
|
||||
for idx := 0; idx < 1000000; idx++ {
|
||||
_ = world.Update(timeDelta)
|
||||
}
|
||||
|
||||
if pos.EqualsApprox(vel.Vector.Clone().Scale(2)) {
|
||||
fmtStr := "position component not updated, expected (%v,%v) but got (%v,%v)"
|
||||
t.Errorf(fmtStr, expectX, expectY, pos.X(), pos.Y())
|
||||
}
|
||||
}
|
75
d2core/d2systems/movement.go
Normal file
75
d2core/d2systems/movement.go
Normal file
@ -0,0 +1,75 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
// NewMovementSystem creates a movement system
|
||||
func NewMovementSystem() *MovementSystem {
|
||||
cfg := ecs.NewFilter().Require(d2components.Position, d2components.Velocity)
|
||||
|
||||
filter := cfg.Build()
|
||||
|
||||
return &MovementSystem{
|
||||
SubscriberSystem: ecs.NewSubscriberSystem(filter),
|
||||
}
|
||||
}
|
||||
|
||||
// static check that MovementSystem implements the System interface
|
||||
var _ ecs.System = &MovementSystem{}
|
||||
|
||||
// MovementSystem handles entity movement based on velocity and position components
|
||||
type MovementSystem struct {
|
||||
*ecs.SubscriberSystem
|
||||
positions *d2components.PositionMap
|
||||
velocities *d2components.VelocityMap
|
||||
}
|
||||
|
||||
// Init initializes the system with the given world
|
||||
func (m *MovementSystem) Init(world *ecs.World) {
|
||||
m.World = world
|
||||
|
||||
if world == nil {
|
||||
m.SetActive(false)
|
||||
return
|
||||
}
|
||||
|
||||
for subIdx := range m.Subscriptions {
|
||||
m.AddSubscription(m.Subscriptions[subIdx])
|
||||
}
|
||||
|
||||
// try to inject the components we require, then cast the returned
|
||||
// abstract ComponentMap back to the concrete implementation
|
||||
m.positions = m.InjectMap(d2components.Position).(*d2components.PositionMap)
|
||||
m.velocities = m.InjectMap(d2components.Velocity).(*d2components.VelocityMap)
|
||||
}
|
||||
|
||||
// Process processes all of the Entities
|
||||
func (m *MovementSystem) Process() {
|
||||
for subIdx := range m.Subscriptions {
|
||||
entities := m.Subscriptions[subIdx].GetEntities()
|
||||
for entIdx := range entities {
|
||||
m.ProcessEntity(entities[entIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessEntity updates an individual entity in the movement system
|
||||
func (m *MovementSystem) ProcessEntity(id ecs.EID) {
|
||||
position, found := m.positions.GetPosition(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
velocity, found := m.velocities.GetVelocity(id)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
s := float64(m.World.TimeDelta) / float64(time.Second)
|
||||
position.Vector = *position.Vector.Add(velocity.Vector.Clone().Scale(s))
|
||||
}
|
154
d2core/d2systems/movement_test.go
Normal file
154
d2core/d2systems/movement_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
|
||||
)
|
||||
|
||||
func TestMovementSystem_Init(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
cfg.With(NewMovementSystem())
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
if len(world.Systems) != 1 {
|
||||
t.Error("system not added to the world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovementSystem_Active(t *testing.T) {
|
||||
movement := NewMovementSystem()
|
||||
|
||||
if movement.Active() {
|
||||
t.Error("system should not be active at creation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovementSystem_SetActive(t *testing.T) {
|
||||
movement := NewMovementSystem()
|
||||
|
||||
movement.SetActive(false)
|
||||
|
||||
if movement.Active() {
|
||||
t.Error("system should be inactive after being set inactive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovementSystem_EntityAdded(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
movement := NewMovementSystem()
|
||||
|
||||
cfg.With(movement).
|
||||
With(d2components.NewPositionMap()).
|
||||
With(d2components.NewVelocityMap())
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
e := world.NewEntity()
|
||||
|
||||
position := movement.positions.AddPosition(e)
|
||||
velocity := movement.velocities.AddVelocity(e)
|
||||
|
||||
px, py := 10., 10.
|
||||
vx, vy := 1., 0.
|
||||
|
||||
position.Set(px, py)
|
||||
velocity.Set(vx, vy)
|
||||
|
||||
if len(movement.Subscriptions[0].GetEntities()) != 1 {
|
||||
t.Error("entity not added to the system")
|
||||
}
|
||||
|
||||
if p, found := movement.positions.GetPosition(e); !found {
|
||||
t.Error("position component not found")
|
||||
} else if p.X() != px || p.Y() != py {
|
||||
fmtError := "position component values incorrect:\n\t expected %v, %v but got %v, %v"
|
||||
t.Errorf(fmtError, px, py, p.X(), p.Y())
|
||||
}
|
||||
|
||||
if v, found := movement.velocities.GetVelocity(e); !found {
|
||||
t.Error("position component not found")
|
||||
} else if v.X() != vx || v.Y() != vy {
|
||||
fmtError := "velocity component values incorrect:\n\t expected %v, %v but got %v, %v"
|
||||
t.Errorf(fmtError, px, py, v.X(), v.Y())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovementSystem_Update(t *testing.T) {
|
||||
// world bootstrap
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
movementSystem := NewMovementSystem()
|
||||
positions := d2components.NewPositionMap()
|
||||
velocities := d2components.NewVelocityMap()
|
||||
|
||||
cfg.With(movementSystem).With(positions).With(velocities)
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
// lets make an entity and add some components to it
|
||||
e := world.NewEntity()
|
||||
position := movementSystem.positions.AddPosition(e)
|
||||
velocity := movementSystem.velocities.AddVelocity(e)
|
||||
|
||||
px, py := 10., 10.
|
||||
vx, vy := 1., -1.
|
||||
|
||||
// mutate the components a bit
|
||||
position.Set(px, py)
|
||||
velocity.Set(vx, vy)
|
||||
|
||||
// should apply the velocity to the position
|
||||
_ = world.Update(time.Second)
|
||||
|
||||
if position.X() != px+vx || position.Y() != py+vy {
|
||||
fmtError := "expected position (%v, %v) but got (%v, %v)"
|
||||
t.Errorf(fmtError, px+vx, py+vy, position.X(), position.Y())
|
||||
}
|
||||
}
|
||||
|
||||
func bench_N_entities(n int, b *testing.B) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
movementSystem := NewMovementSystem()
|
||||
|
||||
cfg.With(movementSystem)
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
for idx := 0; idx < n; idx++ {
|
||||
e := world.NewEntity()
|
||||
p := movementSystem.positions.AddPosition(e)
|
||||
v := movementSystem.velocities.AddVelocity(e)
|
||||
|
||||
p.Set(0, 0)
|
||||
v.Set(rand.Float64(), rand.Float64())
|
||||
}
|
||||
|
||||
benchName := strconv.Itoa(n) + "_entity update"
|
||||
b.Run(benchName, func(b *testing.B) {
|
||||
for idx := 0; idx < b.N; idx++ {
|
||||
_ = world.Update(time.Millisecond)
|
||||
}
|
||||
})
|
||||
|
||||
fmt.Println("done!")
|
||||
}
|
||||
|
||||
func BenchmarkMovementSystem_Update(b *testing.B) {
|
||||
bench_N_entities(1e1, b)
|
||||
bench_N_entities(1e2, b)
|
||||
bench_N_entities(1e3, b)
|
||||
bench_N_entities(1e4, b)
|
||||
bench_N_entities(1e5, b)
|
||||
bench_N_entities(1e6, b)
|
||||
}
|
1
d2core/d2systems/testdata/testfile_a.txt
vendored
Normal file
1
d2core/d2systems/testdata/testfile_a.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
test a
|
46
d2core/d2systems/timescale.go
Normal file
46
d2core/d2systems/timescale.go
Normal file
@ -0,0 +1,46 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultScale float64 = 1
|
||||
)
|
||||
|
||||
// NewTimeScaleSystem creates a timescale system
|
||||
func NewTimeScaleSystem() *TimeScaleSystem {
|
||||
m := &TimeScaleSystem{
|
||||
BaseSystem: &ecs.BaseSystem{},
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// static check that TimeScaleSystem implements the System interface
|
||||
var _ ecs.System = &TimeScaleSystem{}
|
||||
|
||||
// TimeScaleSystem should be the first system added to the world, and whose only job is to
|
||||
// apply a scalar the world's TimeDelta between frames. It's useful for slowing down or speeding
|
||||
// up the game time without affecting the render rate.
|
||||
type TimeScaleSystem struct {
|
||||
*ecs.BaseSystem
|
||||
scale float64
|
||||
}
|
||||
|
||||
// Init will initialize the TimeScale system
|
||||
func (t *TimeScaleSystem) Init(world *ecs.World) {
|
||||
t.World = world
|
||||
t.scale = defaultScale
|
||||
}
|
||||
|
||||
// Process scales the worlds time delta for this frame
|
||||
func (t *TimeScaleSystem) Process() {
|
||||
if !t.Active() {
|
||||
return
|
||||
}
|
||||
|
||||
t.World.TimeDelta *= time.Duration(t.scale)
|
||||
}
|
41
d2core/d2systems/timescale_test.go
Normal file
41
d2core/d2systems/timescale_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
package d2systems
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gravestench/ecs"
|
||||
)
|
||||
|
||||
func TestTimeScaleSystem_Init(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
cfg.With(NewTimeScaleSystem())
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
if len(world.Systems) != 1 {
|
||||
t.Error("system not added to the world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeScaleSystem_Process(t *testing.T) {
|
||||
cfg := ecs.NewWorldConfig()
|
||||
|
||||
timescaleSystem := NewTimeScaleSystem()
|
||||
|
||||
cfg.With(timescaleSystem)
|
||||
|
||||
timescaleSystem.scale = 0.01
|
||||
|
||||
world := ecs.NewWorld(cfg)
|
||||
|
||||
actual := time.Second
|
||||
expected := time.Duration(timescaleSystem.scale) * actual
|
||||
|
||||
world.Update(actual)
|
||||
|
||||
if world.TimeDelta != expected {
|
||||
t.Error("world time delta not scaled")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user