2019-10-24 09:31:59 -04:00
|
|
|
package OpenDiablo2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2019-10-24 11:26:07 -04:00
|
|
|
"github.com/essial/OpenDiablo2/Common"
|
2019-10-25 18:40:27 -04:00
|
|
|
"github.com/essial/OpenDiablo2/Palettes"
|
2019-10-24 11:26:07 -04:00
|
|
|
"github.com/essial/OpenDiablo2/ResourcePaths"
|
2019-10-25 19:12:42 -04:00
|
|
|
"github.com/essial/OpenDiablo2/UI"
|
2019-10-24 09:31:59 -04:00
|
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten"
|
2019-10-25 15:06:47 -04:00
|
|
|
"github.com/hajimehoshi/ebiten/audio"
|
|
|
|
"github.com/hajimehoshi/ebiten/audio/wav"
|
2019-10-24 09:31:59 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// EngineConfig defines the configuration for the engine, loaded from config.json
|
|
|
|
type EngineConfig struct {
|
|
|
|
FullScreen bool
|
|
|
|
Scale float64
|
|
|
|
RunInBackground bool
|
|
|
|
TicksPerSecond int
|
|
|
|
VsyncEnabled bool
|
|
|
|
MpqPath string
|
|
|
|
MpqLoadOrder []string
|
|
|
|
}
|
|
|
|
|
2019-10-25 18:40:27 -04:00
|
|
|
// CursorButton represents a mouse button
|
2019-10-25 15:06:47 -04:00
|
|
|
type CursorButton uint8
|
|
|
|
|
|
|
|
const (
|
2019-10-25 18:40:27 -04:00
|
|
|
// CursorButtonLeft represents the left mouse button
|
|
|
|
CursorButtonLeft CursorButton = 1
|
|
|
|
// CursorButtonRight represents the right mouse button
|
2019-10-25 15:06:47 -04:00
|
|
|
CursorButtonRight CursorButton = 2
|
|
|
|
)
|
|
|
|
|
2019-10-25 18:40:27 -04:00
|
|
|
// Engine is the core OpenDiablo2 engine
|
2019-10-24 09:31:59 -04:00
|
|
|
type Engine struct {
|
2019-10-25 19:12:42 -04:00
|
|
|
Settings EngineConfig // Engine configuration settings from json file
|
|
|
|
Files map[string]string // Map that defines which files are in which MPQs
|
|
|
|
Palettes map[Palettes.Palette]Common.Palette // Color palettes
|
|
|
|
SoundEntries map[string]SoundEntry // Sound configurations
|
|
|
|
LoadingSprite *Common.Sprite // The sprite shown when loading stuff
|
|
|
|
CursorX int // X position of the cursor
|
|
|
|
CursorY int // Y position of the cursor
|
|
|
|
CursorButtons CursorButton // The buttons that are currently being pressed
|
|
|
|
LoadingProgress float64 // LoadingProcess is a range between 0.0 and 1.0. If set, loading screen displays.
|
|
|
|
CurrentScene Common.SceneInterface // The current scene being rendered
|
|
|
|
UIManager *UI.Manager // The UI manager
|
|
|
|
nextScene Common.SceneInterface // The next scene to be loaded at the end of the game loop
|
|
|
|
fontCache map[string]*MPQFont // The font cash
|
|
|
|
audioContext *audio.Context // The Audio context
|
|
|
|
bgmAudio *audio.Player // The audio player
|
|
|
|
fullscreenKey bool // When true, the fullscreen toggle is still being pressed
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// CreateEngine creates and instance of the OpenDiablo2 engine
|
|
|
|
func CreateEngine() *Engine {
|
|
|
|
result := &Engine{
|
|
|
|
LoadingProgress: float64(0.0),
|
|
|
|
CurrentScene: nil,
|
|
|
|
nextScene: nil,
|
|
|
|
fontCache: make(map[string]*MPQFont),
|
|
|
|
}
|
|
|
|
result.loadConfigurationFile()
|
|
|
|
result.mapMpqFiles()
|
|
|
|
result.loadPalettes()
|
|
|
|
result.loadSoundEntries()
|
2019-10-25 19:12:42 -04:00
|
|
|
result.UIManager = UI.CreateManager(result)
|
2019-10-25 17:09:07 -04:00
|
|
|
audioContext, err := audio.NewContext(22050)
|
2019-10-25 15:06:47 -04:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
result.audioContext = audioContext
|
2019-10-25 18:40:27 -04:00
|
|
|
result.LoadingSprite = result.LoadSprite(ResourcePaths.LoadingScreen, Palettes.Loading)
|
2019-10-24 09:31:59 -04:00
|
|
|
loadingSpriteSizeX, loadingSpriteSizeY := result.LoadingSprite.GetSize()
|
|
|
|
result.LoadingSprite.MoveTo(int(400-(loadingSpriteSizeX/2)), int(300+(loadingSpriteSizeY/2)))
|
|
|
|
result.SetNextScene(CreateMainMenu(result))
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Engine) loadConfigurationFile() {
|
|
|
|
log.Println("loading configuration file")
|
|
|
|
configJSON, err := ioutil.ReadFile("config.json")
|
|
|
|
if err != nil {
|
2019-10-25 15:06:47 -04:00
|
|
|
log.Fatal(err)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
var config EngineConfig
|
|
|
|
|
|
|
|
json.Unmarshal(configJSON, &config)
|
|
|
|
v.Settings = config
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Engine) mapMpqFiles() {
|
|
|
|
log.Println("mapping mpq file structure")
|
|
|
|
v.Files = make(map[string]string)
|
|
|
|
lock := sync.RWMutex{}
|
|
|
|
for _, mpqFileName := range v.Settings.MpqLoadOrder {
|
|
|
|
mpqPath := path.Join(v.Settings.MpqPath, mpqFileName)
|
|
|
|
mpq, err := LoadMPQ(mpqPath)
|
|
|
|
if err != nil {
|
2019-10-25 15:06:47 -04:00
|
|
|
log.Fatal(err)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
fileListText, err := mpq.ReadFile("(listfile)")
|
|
|
|
if err != nil {
|
2019-10-25 15:06:47 -04:00
|
|
|
log.Fatal(err)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
fileList := strings.Split(string(fileListText), "\r\n")
|
|
|
|
for _, filePath := range fileList {
|
|
|
|
if _, exists := v.Files[strings.ToLower(filePath)]; exists {
|
|
|
|
lock.RUnlock()
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
v.Files[`/`+strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)] = mpqPath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetFile loads a file from the specified mpq and returns the data as a byte array
|
|
|
|
func (v *Engine) GetFile(fileName string) []byte {
|
|
|
|
// TODO: May want to cache some things if performance becomes an issue
|
|
|
|
mpqFile := v.Files[strings.ToLower(fileName)]
|
|
|
|
mpq, err := LoadMPQ(mpqFile)
|
|
|
|
if err != nil {
|
2019-10-25 15:06:47 -04:00
|
|
|
log.Fatal(err)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
2019-10-25 15:06:47 -04:00
|
|
|
fileName = strings.ReplaceAll(fileName, `/`, `\`)[1:]
|
|
|
|
blockTableEntry, err := mpq.getFileBlockData(fileName)
|
2019-10-24 09:31:59 -04:00
|
|
|
if err != nil {
|
2019-10-25 15:06:47 -04:00
|
|
|
log.Fatal(err)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
mpqStream := CreateMPQStream(mpq, blockTableEntry, fileName)
|
|
|
|
result := make([]byte, blockTableEntry.UncompressedFileSize)
|
|
|
|
mpqStream.Read(result, 0, blockTableEntry.UncompressedFileSize)
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsLoading returns true if the engine is currently in a loading state
|
|
|
|
func (v *Engine) IsLoading() bool {
|
|
|
|
return v.LoadingProgress < 1.0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Engine) loadPalettes() {
|
2019-10-25 19:12:42 -04:00
|
|
|
v.Palettes = make(map[Palettes.Palette]Common.Palette)
|
2019-10-24 09:31:59 -04:00
|
|
|
log.Println("loading palettes")
|
|
|
|
for file := range v.Files {
|
|
|
|
if strings.Index(file, "/data/global/palette/") != 0 || strings.Index(file, ".dat") != len(file)-4 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
nameParts := strings.Split(file, `/`)
|
2019-10-25 18:40:27 -04:00
|
|
|
paletteName := Palettes.Palette(nameParts[len(nameParts)-2])
|
2019-10-25 19:12:42 -04:00
|
|
|
palette := Common.CreatePalette(paletteName, v.GetFile(file))
|
2019-10-24 09:31:59 -04:00
|
|
|
v.Palettes[paletteName] = palette
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Engine) loadSoundEntries() {
|
|
|
|
log.Println("loading sound configurations")
|
|
|
|
v.SoundEntries = make(map[string]SoundEntry)
|
|
|
|
soundData := strings.Split(string(v.GetFile(ResourcePaths.SoundSettings)), "\r\n")[1:]
|
|
|
|
for _, line := range soundData {
|
|
|
|
if len(line) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
soundEntry := CreateSoundEntry(line)
|
|
|
|
v.SoundEntries[soundEntry.Handle] = soundEntry
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadSprite loads a sprite from the game's data files
|
2019-10-25 19:12:42 -04:00
|
|
|
func (v *Engine) LoadSprite(fileName string, palette Palettes.Palette) *Common.Sprite {
|
2019-10-24 09:31:59 -04:00
|
|
|
data := v.GetFile(fileName)
|
2019-10-25 19:12:42 -04:00
|
|
|
sprite := Common.CreateSprite(data, v.Palettes[palette])
|
2019-10-24 09:31:59 -04:00
|
|
|
return sprite
|
|
|
|
}
|
|
|
|
|
|
|
|
// updateScene handles the scene maintenance for the engine
|
|
|
|
func (v *Engine) updateScene() {
|
|
|
|
if v.nextScene == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if v.CurrentScene != nil {
|
|
|
|
v.CurrentScene.Unload()
|
|
|
|
}
|
|
|
|
v.CurrentScene = v.nextScene
|
|
|
|
v.nextScene = nil
|
2019-10-25 19:12:42 -04:00
|
|
|
v.UIManager.Reset()
|
2019-10-24 09:31:59 -04:00
|
|
|
v.CurrentScene.Load()
|
|
|
|
}
|
|
|
|
|
2019-10-25 15:06:47 -04:00
|
|
|
// CursorButtonPressed determines if the specified button has been pressed
|
|
|
|
func (v *Engine) CursorButtonPressed(button CursorButton) bool {
|
|
|
|
return v.CursorButtons&button > 0
|
|
|
|
}
|
|
|
|
|
2019-10-24 09:31:59 -04:00
|
|
|
// Update updates the internal state of the engine
|
|
|
|
func (v *Engine) Update() {
|
2019-10-25 17:15:44 -04:00
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyAlt) && ebiten.IsKeyPressed(ebiten.KeyEnter) {
|
|
|
|
if !v.fullscreenKey {
|
|
|
|
ebiten.SetFullscreen(!ebiten.IsFullscreen())
|
|
|
|
}
|
|
|
|
v.fullscreenKey = true
|
|
|
|
} else {
|
|
|
|
v.fullscreenKey = false
|
|
|
|
}
|
|
|
|
|
2019-10-24 09:31:59 -04:00
|
|
|
v.updateScene()
|
|
|
|
if v.CurrentScene == nil {
|
2019-10-25 15:06:47 -04:00
|
|
|
log.Fatal("no scene loaded")
|
|
|
|
}
|
2019-10-25 19:12:42 -04:00
|
|
|
|
|
|
|
if v.IsLoading() {
|
|
|
|
return
|
|
|
|
}
|
2019-10-25 15:06:47 -04:00
|
|
|
v.CursorButtons = 0
|
|
|
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
|
|
|
v.CursorButtons |= CursorButtonLeft
|
|
|
|
}
|
|
|
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
|
|
|
|
v.CursorButtons |= CursorButtonRight
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
v.CurrentScene.Update()
|
2019-10-25 19:12:42 -04:00
|
|
|
v.UIManager.Update()
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Draw draws the game
|
|
|
|
func (v *Engine) Draw(screen *ebiten.Image) {
|
|
|
|
v.CursorX, v.CursorY = ebiten.CursorPosition()
|
|
|
|
if v.LoadingProgress < 1.0 {
|
2019-10-25 19:12:42 -04:00
|
|
|
v.LoadingSprite.Frame = uint8(Common.Max(0, Common.Min(uint32(len(v.LoadingSprite.Frames)-1), uint32(float64(len(v.LoadingSprite.Frames)-1)*v.LoadingProgress))))
|
2019-10-24 09:31:59 -04:00
|
|
|
v.LoadingSprite.Draw(screen)
|
|
|
|
} else {
|
|
|
|
if v.CurrentScene == nil {
|
2019-10-25 15:06:47 -04:00
|
|
|
log.Fatal("no scene loaded")
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
v.CurrentScene.Render(screen)
|
2019-10-25 19:12:42 -04:00
|
|
|
v.UIManager.Draw(screen)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetNextScene tells the engine what scene to load on the next update cycle
|
|
|
|
func (v *Engine) SetNextScene(nextScene Common.SceneInterface) {
|
|
|
|
v.nextScene = nextScene
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetFont creates or loads an existing font
|
2019-10-25 18:40:27 -04:00
|
|
|
func (v *Engine) GetFont(font string, palette Palettes.Palette) *MPQFont {
|
|
|
|
cacheItem, exists := v.fontCache[font+"_"+string(palette)]
|
2019-10-24 09:31:59 -04:00
|
|
|
if exists {
|
|
|
|
return cacheItem
|
|
|
|
}
|
2019-10-25 18:40:27 -04:00
|
|
|
newFont := CreateMPQFont(v, font, palette)
|
|
|
|
v.fontCache[font+"_"+string(palette)] = newFont
|
2019-10-24 09:31:59 -04:00
|
|
|
return newFont
|
|
|
|
}
|
2019-10-25 15:06:47 -04:00
|
|
|
|
|
|
|
// PlayBGM plays an infinitely looping background track
|
|
|
|
func (v *Engine) PlayBGM(song string) {
|
|
|
|
go func() {
|
|
|
|
if v.bgmAudio != nil {
|
|
|
|
v.bgmAudio.Close()
|
|
|
|
}
|
|
|
|
audioData := v.GetFile(song)
|
|
|
|
d, err := wav.Decode(v.audioContext, audio.BytesReadSeekCloser(audioData))
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2019-10-25 17:09:07 -04:00
|
|
|
s := audio.NewInfiniteLoop(d, int64(len(audioData)))
|
2019-10-25 15:06:47 -04:00
|
|
|
|
2019-10-25 17:09:07 -04:00
|
|
|
v.bgmAudio, err = audio.NewPlayer(v.audioContext, s)
|
2019-10-25 15:06:47 -04:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
// Play the infinite-length stream. This never ends.
|
|
|
|
v.bgmAudio.Rewind()
|
|
|
|
v.bgmAudio.Play()
|
|
|
|
}()
|
|
|
|
}
|