1
1
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:
dknuth 2020-10-10 19:49:17 -07:00 committed by gravestench
parent 3f8dcf2232
commit b1bf6993d2
30 changed files with 2319 additions and 1 deletions

View 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
)

View 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")
}
}

View 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
}

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

View File

@ -0,0 +1,6 @@
package d2events
type EventListener struct {
fn func(...interface{})
once bool
}

View 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")
}
}

View 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
)

View File

@ -0,0 +1,4 @@
package d2components
type StringTableAsset struct {
}

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

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

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

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

View 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
}

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

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

View File

@ -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

View 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 {
//
//}

View 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
}
}

View 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`")
}
}

View 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
}

View 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")
}
}

View 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
}
}
}

View 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")
}
}

View 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
}
}
}

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

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

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

View File

@ -0,0 +1 @@
test a

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

View 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")
}
}