2019-11-10 03:36:53 -05:00
|
|
|
package d2core
|
2019-10-31 21:17:13 -04:00
|
|
|
|
|
|
|
import (
|
2019-11-11 23:48:55 -05:00
|
|
|
"io/ioutil"
|
2019-10-31 21:17:13 -04:00
|
|
|
"log"
|
2019-11-11 23:48:55 -05:00
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"runtime"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2019-10-31 21:17:13 -04:00
|
|
|
"time"
|
2019-11-11 23:48:55 -05:00
|
|
|
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
|
|
|
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
2019-10-31 21:17:13 -04:00
|
|
|
)
|
|
|
|
|
2019-11-11 23:48:55 -05:00
|
|
|
/*
|
|
|
|
File Spec
|
|
|
|
--------------------------------------------
|
|
|
|
UINT32 GameState Version
|
|
|
|
INT64 Game Seed
|
|
|
|
BYTE Hero Type
|
|
|
|
BYTE Act
|
|
|
|
BYTE Hero Name Length
|
|
|
|
BYTE[] Hero Name
|
|
|
|
--------------------------------------------
|
|
|
|
*/
|
|
|
|
|
2019-10-31 21:17:13 -04:00
|
|
|
type GameState struct {
|
2019-11-11 23:48:55 -05:00
|
|
|
Seed int64
|
|
|
|
HeroName string
|
|
|
|
HeroType d2enum.Hero
|
|
|
|
Act int
|
|
|
|
FilePath string
|
|
|
|
}
|
|
|
|
|
|
|
|
const GameStateVersion = uint32(1) // Update this when you make breaking changes
|
|
|
|
|
|
|
|
func HasGameStates() bool {
|
|
|
|
files, _ := ioutil.ReadDir(getGameBaseSavePath())
|
|
|
|
return len(files) > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetAllGameStates() []*GameState {
|
|
|
|
// TODO: Make this not crash tf out on bad files
|
|
|
|
basePath := getGameBaseSavePath()
|
|
|
|
files, _ := ioutil.ReadDir(basePath)
|
|
|
|
result := make([]*GameState, 0)
|
|
|
|
for _, file := range files {
|
|
|
|
fileName := file.Name()
|
|
|
|
if file.IsDir() || len(fileName) < 5 || strings.ToLower(fileName[len(fileName)-4:]) != ".od2" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
gameState := LoadGameState(path.Join(basePath, file.Name()))
|
|
|
|
if gameState == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
result = append(result, gameState)
|
|
|
|
}
|
|
|
|
return result
|
2019-10-31 21:17:13 -04:00
|
|
|
}
|
|
|
|
|
2019-11-11 23:48:55 -05:00
|
|
|
// CreateTestGameState is used for the map engine previewer
|
|
|
|
func CreateTestGameState() *GameState {
|
2019-10-31 21:17:13 -04:00
|
|
|
result := &GameState{
|
|
|
|
Seed: time.Now().UnixNano(),
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func LoadGameState(path string) *GameState {
|
2019-11-11 23:48:55 -05:00
|
|
|
result := &GameState{
|
|
|
|
FilePath: path,
|
|
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
|
|
|
bytes, err := ioutil.ReadAll(f)
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
2019-11-12 23:44:04 -05:00
|
|
|
defer f.Close()
|
2019-11-11 23:48:55 -05:00
|
|
|
sr := d2common.CreateStreamReader(bytes)
|
|
|
|
if sr.GetUInt32() > GameStateVersion {
|
|
|
|
// Unknown game version
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
result.Seed = sr.GetInt64()
|
|
|
|
result.HeroType = d2enum.Hero(sr.GetByte())
|
|
|
|
result.Act = int(sr.GetByte())
|
|
|
|
heroNameLen := sr.GetByte()
|
|
|
|
heroName, _ := sr.ReadBytes(int(heroNameLen))
|
|
|
|
result.HeroName = string(heroName)
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func CreateGameState(heroName string, hero d2enum.Hero, hardcore bool) *GameState {
|
|
|
|
result := &GameState{
|
|
|
|
HeroName: heroName,
|
|
|
|
HeroType: hero,
|
|
|
|
Act: 1,
|
|
|
|
Seed: time.Now().UnixNano(),
|
|
|
|
FilePath: "",
|
|
|
|
}
|
|
|
|
result.Save()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func getGameBaseSavePath() string {
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
appDataPath := os.Getenv("APPDATA")
|
|
|
|
basePath := path.Join(appDataPath, "OpenDiablo2", "Saves")
|
|
|
|
if err := os.MkdirAll(basePath, os.ModeDir); err != nil {
|
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
|
|
|
return basePath
|
|
|
|
}
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
|
|
|
basePath := path.Join(homeDir, ".OpenDiablo2", "Saves")
|
2019-11-12 08:15:50 -05:00
|
|
|
if err := os.MkdirAll(basePath, 0755); err != nil {
|
2019-11-11 23:48:55 -05:00
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
|
|
|
// TODO: Is mac supposed to have a super special place for the save games?
|
|
|
|
return basePath
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFirstFreefileName() string {
|
|
|
|
i := 0
|
|
|
|
basePath := getGameBaseSavePath()
|
|
|
|
for {
|
|
|
|
filePath := path.Join(basePath, strconv.Itoa(i)+".od2")
|
|
|
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
|
|
return filePath
|
|
|
|
}
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *GameState) Save() {
|
|
|
|
if v.FilePath == "" {
|
|
|
|
v.FilePath = getFirstFreefileName()
|
|
|
|
}
|
|
|
|
f, err := os.Create(v.FilePath)
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
sr := d2common.CreateStreamWriter()
|
|
|
|
sr.PushUint32(GameStateVersion)
|
|
|
|
sr.PushInt64(v.Seed)
|
|
|
|
sr.PushByte(byte(v.HeroType))
|
|
|
|
sr.PushByte(byte(v.Act))
|
|
|
|
sr.PushByte(byte(len(v.HeroName)))
|
|
|
|
for _, ch := range v.HeroName {
|
|
|
|
sr.PushByte(byte(ch))
|
|
|
|
}
|
|
|
|
if _, err := f.Write(sr.GetBytes()); err != nil {
|
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
2019-10-31 21:17:13 -04:00
|
|
|
}
|