Merge pull request #1023 from ianling/ecs

Backmerge master into ecs
This commit is contained in:
gravestench 2021-01-09 20:13:07 +00:00 committed by GitHub
commit 31d8343fd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 2729 additions and 2395 deletions

View File

@ -1,10 +1,9 @@
---
name: pull_request
"on": [pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
name: ''
runs-on: self-hosted
continue-on-error: true
steps:
- name: Set up Go 1.14

View File

@ -1,39 +0,0 @@
---
name: build
"on":
push:
branches:
- master
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.14
uses: actions/setup-go@v2.1.3
with:
go-version: 1.14
id: go
- name: Check out code
uses: actions/checkout@v2.3.4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y xvfb libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
- name: Run golangci-lint
continue-on-error: false
uses: golangci/golangci-lint-action@v2.3.0
with:
version: v1.32
- name: Run tests
env:
DISPLAY: ":99.0"
run: |
xvfb-run --auto-servernum go test -v -race ./...
- name: Build binary
run: go build .

View File

@ -25,7 +25,8 @@ ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
## Status
At the moment (october 2020) the game starts, you can select any character and run around Act1 town.
At the moment (december 2020) the game starts, you can select any character and run around Act1 town.
You can also open any of the game's panels.
Much work has been made in the background, but a lot of work still has to be done for the game to be playable.
@ -128,6 +129,8 @@ which will be updated over time with new requirements.
![Inventory Window](docs/Inventory.png)
![Game Panels](docs/game_panels.png)
## Additional Credits
- Diablo2 Logo

View File

@ -6,6 +6,7 @@ import (
"container/ring"
"encoding/json"
"errors"
"flag"
"fmt"
"image"
"image/gif"
@ -24,7 +25,6 @@ import (
"github.com/pkg/profile"
"golang.org/x/image/colornames"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
@ -85,17 +85,10 @@ type App struct {
// Options is used to store all of the app options that can be set with arguments
type Options struct {
printVersion *bool
Debug *bool
profiler *string
Server *d2networking.ServerOptions
LogLevel *d2util.LogLevel
}
type bindTerminalEntry struct {
name string
description string
action interface{}
Debug *bool
profiler *string
Server *d2networking.ServerOptions
LogLevel *d2util.LogLevel
}
const (
@ -110,21 +103,24 @@ const (
// Create creates a new instance of the application
func Create(gitBranch, gitCommit string) *App {
assetManager, assetError := d2asset.NewAssetManager()
logger := d2util.NewLogger()
logger.SetPrefix(appLoggerPrefix)
app := &App{
Logger: logger,
gitBranch: gitBranch,
gitCommit: gitCommit,
asset: assetManager,
Options: &Options{
Server: &d2networking.ServerOptions{},
},
errorMessage: assetError,
}
app.Infof("OpenDiablo2 - Open source Diablo 2 engine")
app.Logger = d2util.NewLogger()
app.Logger.SetPrefix(appLoggerPrefix)
app.Logger.SetLevel(d2util.LogLevelNone)
app.parseArguments()
app.SetLevel(*app.Options.LogLevel)
app.asset, app.errorMessage = d2asset.NewAssetManager(*app.Options.LogLevel)
return app
}
@ -140,7 +136,7 @@ func (a *App) startDedicatedServer() error {
srvChanIn := make(chan int)
srvChanLog := make(chan string)
srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, a.config.LogLevel, maxPlayers)
srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, *a.Options.LogLevel, maxPlayers)
if srvErr != nil {
return srvErr
}
@ -173,15 +169,7 @@ func (a *App) loadEngine() error {
return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2")
}
// if the log level was specified at the command line, use it
logLevel := *a.Options.LogLevel
if logLevel == d2util.LogLevelUnspecified {
logLevel = a.config.LogLevel
}
a.asset.SetLogLevel(logLevel)
audio := ebiten2.CreateAudio(a.config.LogLevel, a.asset)
audio := ebiten2.CreateAudio(*a.Options.LogLevel, a.asset)
inputManager := d2input.NewInputManager()
@ -190,14 +178,9 @@ func (a *App) loadEngine() error {
return err
}
err = a.asset.BindTerminalCommands(term)
if err != nil {
return err
}
scriptEngine := d2script.CreateScriptEngine()
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, a.config.LogLevel, audio)
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio)
a.inputManager = inputManager
a.terminal = term
@ -206,50 +189,48 @@ func (a *App) loadEngine() error {
a.ui = uiManager
a.tAllocSamples = createZeroedRing(nSamplesTAlloc)
if a.gitBranch == "" {
a.gitBranch = "Local Build"
}
return nil
}
func (a *App) parseArguments() {
const (
versionArg = "version"
versionShort = 'v'
versionDesc = "Prints the version of the app"
profilerArg = "profile"
profilerDesc = "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)"
serverArg = "dedicated"
serverShort = 'd'
serverDesc = "Starts a dedicated server"
playersArg = "players"
playersDesc = "Sets the number of max players for the dedicated server"
loggingArg = "loglevel"
loggingShort = 'l'
loggingDesc = "Enables verbose logging. Log levels will include those below it. " +
"0 disables log messages, " +
"1 shows errors, " +
"2 shows warnings, " +
"3 shows info, " +
"4 shows debug" +
"5 uses value from config file (default)"
descProfile = "Profiles the program,\none of (cpu, mem, block, goroutine, trace, thread, mutex)"
descPlayers = "Sets the number of max players for the dedicated server"
descLogging = "Enables verbose logging. Log levels will include those below it.\n" +
" 0 disables log messages\n" +
" 1 shows fatal\n" +
" 2 shows error\n" +
" 3 shows warning\n" +
" 4 shows info\n" +
" 5 shows debug\n"
)
a.Options.profiler = kingpin.Flag(profilerArg, profilerDesc).String()
a.Options.Server.Dedicated = kingpin.Flag(serverArg, serverDesc).Short(serverShort).Bool()
a.Options.printVersion = kingpin.Flag(versionArg, versionDesc).Short(versionShort).Bool()
a.Options.Server.MaxPlayers = kingpin.Flag(playersArg, playersDesc).Int()
a.Options.LogLevel = kingpin.Flag(loggingArg, loggingDesc).
Short(loggingShort).
Default(strconv.Itoa(d2util.LogLevelUnspecified)).
Int()
a.Options.profiler = flag.String("profile", "", descProfile)
a.Options.Server.Dedicated = flag.Bool("dedicated", false, "Starts a dedicated server")
a.Options.Server.MaxPlayers = flag.Int("players", 0, descPlayers)
a.Options.LogLevel = flag.Int("l", d2util.LogLevelDefault, descLogging)
showVersion := flag.Bool("v", false, "Show version")
showHelp := flag.Bool("h", false, "Show help")
kingpin.Parse()
flag.Usage = func() {
fmt.Printf("usage: %s [<flags>]\n\nFlags:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if *a.Options.LogLevel >= d2util.LogLevelUnspecified {
*a.Options.LogLevel = d2util.LogLevelDefault
}
if *showVersion {
a.Infof("version: OpenDiablo2 (%s %s)", a.gitBranch, a.gitCommit)
os.Exit(0)
}
if *showHelp {
flag.Usage()
os.Exit(0)
}
}
// LoadConfig loads the OpenDiablo2 config file
@ -287,45 +268,15 @@ func (a *App) LoadConfig() (*d2config.Configuration, error) {
}
// Run executes the application and kicks off the entire game process
func (a *App) Run() error {
a.parseArguments()
func (a *App) Run() (err error) {
// add our possible config directories
_, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()))
_, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()))
config, err := a.LoadConfig()
if err != nil {
if a.config, err = a.LoadConfig(); err != nil {
return err
}
a.config = config
a.asset.SetLogLevel(config.LogLevel)
// print version and exit if `--version` was supplied
if *a.Options.printVersion {
fmtVersion := "OpenDiablo2 (%s %s)"
if a.gitBranch == "" {
a.gitBranch = "local"
}
if a.gitCommit == "" {
a.gitCommit = "build"
}
fmt.Printf(fmtVersion, a.gitBranch, a.gitCommit)
os.Exit(0)
}
logLevel := *a.Options.LogLevel
if logLevel == d2util.LogLevelUnspecified {
logLevel = a.config.LogLevel
}
a.asset.SetLogLevel(logLevel)
// start profiler if argument was supplied
if len(*a.Options.profiler) > 0 {
profiler := enableProfiler(*a.Options.profiler, a)
@ -389,36 +340,39 @@ func (a *App) initialize() error {
a.renderer.SetWindowIcon("d2logo.png")
a.terminal.BindLogger()
terminalActions := [...]bindTerminalEntry{
{"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap},
{"fullscreen", "toggles fullscreen", a.toggleFullScreen},
{"capframe", "captures a still frame", a.setupCaptureFrame},
{"capgifstart", "captures an animation (start)", a.startAnimationCapture},
{"capgifstop", "captures an animation (stop)", a.stopAnimationCapture},
{"vsync", "toggles vsync", a.toggleVsync},
{"fps", "toggle fps counter", a.toggleFpsCounter},
{"timescale", "set scalar for elapsed time", a.setTimeScale},
{"quit", "exits the game", a.quitGame},
{"screen-gui", "enters the gui playground screen", a.enterGuiPlayground},
{"js", "eval JS scripts", a.evalJS},
terminalCommands := []struct {
name string
desc string
args []string
fn func(args []string) error
}{
{"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap},
{"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen},
{"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame},
{"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture},
{"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture},
{"vsync", "toggles vsync", nil, a.toggleVsync},
{"fps", "toggle fps counter", nil, a.toggleFpsCounter},
{"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale},
{"quit", "exits the game", nil, a.quitGame},
{"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground},
{"js", "eval JS scripts", []string{"code"}, a.evalJS},
}
for idx := range terminalActions {
action := &terminalActions[idx]
if err := a.terminal.BindAction(action.name, action.description, action.action); err != nil {
a.Fatal(err.Error())
for _, cmd := range terminalCommands {
if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil {
a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error())
}
}
gui, err := d2gui.CreateGuiManager(a.asset, a.config.LogLevel, a.inputManager)
gui, err := d2gui.CreateGuiManager(a.asset, *a.Options.LogLevel, a.inputManager)
if err != nil {
return err
}
a.guiManager = gui
a.screen = d2screen.NewScreenManager(a.ui, a.config.LogLevel, a.guiManager)
a.screen = d2screen.NewScreenManager(a.ui, *a.Options.LogLevel, a.guiManager)
a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)
@ -682,7 +636,7 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
return deltaAllocPerFrame * fps / bytesToMegabyte
}
func (a *App) dumpHeap() {
func (a *App) dumpHeap([]string) error {
if _, err := os.Stat("./pprof/"); os.IsNotExist(err) {
if err := os.Mkdir("./pprof/", 0750); err != nil {
a.Fatal(err.Error())
@ -701,48 +655,56 @@ func (a *App) dumpHeap() {
if err := fileOut.Close(); err != nil {
a.Fatal(err.Error())
}
return nil
}
func (a *App) evalJS(code string) {
val, err := a.scriptEngine.Eval(code)
func (a *App) evalJS(args []string) error {
val, err := a.scriptEngine.Eval(args[0])
if err != nil {
a.terminal.OutputErrorf("%s", err)
return
a.terminal.Errorf(err.Error())
return nil
}
a.Info("%s" + val)
return nil
}
func (a *App) toggleFullScreen() {
func (a *App) toggleFullScreen([]string) error {
fullscreen := !a.renderer.IsFullScreen()
a.renderer.SetFullScreen(fullscreen)
a.terminal.OutputInfof("fullscreen is now: %v", fullscreen)
a.terminal.Infof("fullscreen is now: %v", fullscreen)
return nil
}
func (a *App) setupCaptureFrame(path string) {
func (a *App) setupCaptureFrame(args []string) error {
a.captureState = captureStateFrame
a.capturePath = path
a.capturePath = args[0]
a.captureFrames = nil
return nil
}
func (a *App) doCaptureFrame(target d2interface.Surface) error {
fp, err := os.Create(a.capturePath)
if err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return err
}
defer func() {
if err := fp.Close(); err != nil {
a.Fatal(err.Error())
}
}()
screenshot := target.Screenshot()
if err := png.Encode(fp, screenshot); err != nil {
return err
}
a.Info(fmt.Sprintf("saved frame to %s", a.capturePath))
if err := fp.Close(); err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return nil
}
a.terminal.Infof("saved frame to %s", a.capturePath)
return nil
}
@ -802,47 +764,61 @@ func (a *App) convertFramesToGif() error {
return err
}
a.Info(fmt.Sprintf("saved animation to %s", a.capturePath))
a.Infof("saved animation to %s", a.capturePath)
return nil
}
func (a *App) startAnimationCapture(path string) {
func (a *App) startAnimationCapture(args []string) error {
a.captureState = captureStateGif
a.capturePath = path
a.capturePath = args[0]
a.captureFrames = nil
return nil
}
func (a *App) stopAnimationCapture() {
func (a *App) stopAnimationCapture([]string) error {
a.captureState = captureStateNone
return nil
}
func (a *App) toggleVsync() {
func (a *App) toggleVsync([]string) error {
vsync := !a.renderer.GetVSyncEnabled()
a.renderer.SetVSyncEnabled(vsync)
a.terminal.OutputInfof("vsync is now: %v", vsync)
a.terminal.Infof("vsync is now: %v", vsync)
return nil
}
func (a *App) toggleFpsCounter() {
func (a *App) toggleFpsCounter([]string) error {
a.showFPS = !a.showFPS
a.terminal.OutputInfof("fps counter is now: %v", a.showFPS)
a.terminal.Infof("fps counter is now: %v", a.showFPS)
return nil
}
func (a *App) setTimeScale(timeScale float64) {
if timeScale <= 0 {
a.terminal.OutputErrorf("invalid time scale value")
} else {
a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale)
a.timeScale = timeScale
func (a *App) setTimeScale(args []string) error {
timeScale, err := strconv.ParseFloat(args[0], 64)
if err != nil || timeScale <= 0 {
a.terminal.Errorf("invalid time scale value")
return nil
}
a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale)
a.timeScale = timeScale
return nil
}
func (a *App) quitGame() {
func (a *App) quitGame([]string) error {
os.Exit(0)
return nil
}
func (a *App) enterGuiPlayground() {
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, a.config.LogLevel, a.asset))
func (a *App) enterGuiPlayground([]string) error {
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset))
return nil
}
func createZeroedRing(n int) *ring.Ring {
@ -911,7 +887,7 @@ func (a *App) ToMainMenu(errorMessageOptional ...string) {
buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit}
mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo,
a.config.LogLevel, errorMessageOptional...)
*a.Options.LogLevel, errorMessageOptional...)
if err != nil {
a.Error(err.Error())
return
@ -922,7 +898,7 @@ func (a *App) ToMainMenu(errorMessageOptional ...string) {
// ToSelectHero forces the game to transition to the Select Hero (create character) screen
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, a.config.LogLevel, host)
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, *a.Options.LogLevel, host)
if err != nil {
a.Error(err.Error())
return
@ -933,18 +909,18 @@ func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType,
// ToCreateGame forces the game to transition to the Create Game screen
func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) {
gameClient, err := d2client.Create(connType, a.asset, a.config.LogLevel, a.scriptEngine)
gameClient, err := d2client.Create(connType, a.asset, *a.Options.LogLevel, a.scriptEngine)
if err != nil {
a.Error(err.Error())
}
if err = gameClient.Open(host, filePath); err != nil {
errorMessage := fmt.Sprintf("can not connect to the host: %s", host)
fmt.Println(errorMessage)
a.Error(errorMessage)
a.ToMainMenu(errorMessage)
} else {
game, err := d2gamescreen.CreateGame(
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, a.config.LogLevel, a.guiManager,
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, *a.Options.LogLevel, a.guiManager,
)
if err != nil {
a.Error(err.Error())
@ -957,9 +933,9 @@ func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.Clie
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
a.audio, a.ui, connType, a.config.LogLevel, connHost)
a.audio, a.ui, connType, *a.Options.LogLevel, connHost)
if err != nil {
fmt.Printf("unable to create character select screen: %s", err)
a.Errorf("unable to create character select screen: %s", err)
}
a.screen.SetNextScreen(characterSelect)
@ -968,7 +944,7 @@ func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnection
// ToMapEngineTest forces the game to transition to the map engine test screen
func (a *App) ToMapEngineTest(region, level int) {
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio,
a.config.LogLevel, a.screen)
*a.Options.LogLevel, a.screen)
if err != nil {
a.Error(err.Error())
return
@ -979,10 +955,10 @@ func (a *App) ToMapEngineTest(region, level int) {
// ToCredits forces the game to transition to the credits screen
func (a *App) ToCredits() {
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, a.config.LogLevel, a.ui))
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, *a.Options.LogLevel, a.ui))
}
// ToCinematics forces the game to transition to the cinematics menu
func (a *App) ToCinematics() {
a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, a.config.LogLevel, a.ui))
a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, *a.Options.LogLevel, a.ui))
}

View File

@ -4,12 +4,6 @@ import (
"io"
)
const (
bytesPerInt16 = 2
bytesPerInt32 = 4
bytesPerInt64 = 8
)
// StreamReader allows you to read data from a byte array in various formats
type StreamReader struct {
data []byte
@ -26,16 +20,6 @@ func CreateStreamReader(source []byte) *StreamReader {
return result
}
// GetPosition returns the current stream position
func (v *StreamReader) GetPosition() uint64 {
return v.position
}
// GetSize returns the total size of the stream in bytes
func (v *StreamReader) GetSize() uint64 {
return uint64(len(v.data))
}
// GetByte returns a byte from the stream
func (v *StreamReader) GetByte() byte {
result := v.data[v.position]
@ -44,32 +28,46 @@ func (v *StreamReader) GetByte() byte {
return result
}
// GetUInt16 returns a uint16 word from the stream
func (v *StreamReader) GetUInt16() uint16 {
var result uint16
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint16(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt16
return result
}
// GetInt16 returns a int16 word from the stream
func (v *StreamReader) GetInt16() int16 {
var result int16
return int16(v.GetUInt16())
}
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += int16(v.data[v.position+offset]) << shift
}
// GetUInt16 returns a uint16 word from the stream
//nolint
func (v *StreamReader) GetUInt16() uint16 {
b := v.ReadBytes(2)
return uint16(b[0]) | uint16(b[1])<<8
}
v.position += bytesPerInt16
// GetInt32 returns an int32 dword from the stream
func (v *StreamReader) GetInt32() int32 {
return int32(v.GetUInt32())
}
return result
// GetUInt32 returns a uint32 dword from the stream
//nolint
func (v *StreamReader) GetUInt32() uint32 {
b := v.ReadBytes(4)
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
// GetInt64 returns a uint64 qword from the stream
func (v *StreamReader) GetInt64() int64 {
return int64(v.GetUInt64())
}
// GetUInt64 returns a uint64 qword from the stream
//nolint
func (v *StreamReader) GetUInt64() uint64 {
b := v.ReadBytes(8)
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
// GetPosition returns the current stream position
func (v *StreamReader) GetPosition() uint64 {
return v.position
}
// SetPosition sets the stream position with the given position
@ -77,51 +75,9 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
v.position = newPosition
}
// GetUInt32 returns a uint32 dword from the stream
func (v *StreamReader) GetUInt32() uint32 {
var result uint32
for offset := uint64(0); offset < bytesPerInt32; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint32(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt32
return result
}
// GetInt32 returns an int32 dword from the stream
func (v *StreamReader) GetInt32() int32 {
var result int32
for offset := uint64(0); offset < bytesPerInt32; offset++ {
shift := uint8(bitsPerByte * offset)
result += int32(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt32
return result
}
// GetUint64 returns a uint64 qword from the stream
func (v *StreamReader) GetUint64() uint64 {
var result uint64
for offset := uint64(0); offset < bytesPerInt64; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint64(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt64
return result
}
// GetInt64 returns a uint64 qword from the stream
func (v *StreamReader) GetInt64() int64 {
return int64(v.GetUint64())
// GetSize returns the total size of the stream in bytes
func (v *StreamReader) GetSize() uint64 {
return uint64(len(v.data))
}
// ReadByte implements io.ByteReader

View File

@ -2,10 +2,6 @@ package d2datautils
import "bytes"
const (
byteMask = 0xFF
)
// StreamWriter allows you to create a byte array by streaming in writes of various sizes
type StreamWriter struct {
data *bytes.Buffer
@ -20,41 +16,40 @@ func CreateStreamWriter() *StreamWriter {
return result
}
// GetBytes returns the the byte slice of the underlying data
func (v *StreamWriter) GetBytes() []byte {
return v.data.Bytes()
}
// PushByte writes a byte to the stream
func (v *StreamWriter) PushByte(val byte) {
v.data.WriteByte(val)
}
// PushUint16 writes an uint16 word to the stream
func (v *StreamWriter) PushUint16(val uint16) {
for count := 0; count < bytesPerInt16; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
}
// PushInt16 writes a int16 word to the stream
func (v *StreamWriter) PushInt16(val int16) {
for count := 0; count < bytesPerInt16; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
v.PushUint16(uint16(val))
}
// PushUint16 writes an uint16 word to the stream
//nolint
func (v *StreamWriter) PushUint16(val uint16) {
v.data.WriteByte(byte(val))
v.data.WriteByte(byte(val >> 8))
}
// PushInt32 writes a int32 dword to the stream
func (v *StreamWriter) PushInt32(val int32) {
v.PushUint32(uint32(val))
}
// PushUint32 writes a uint32 dword to the stream
//nolint
func (v *StreamWriter) PushUint32(val uint32) {
for count := 0; count < bytesPerInt32; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
}
// PushUint64 writes a uint64 qword to the stream
func (v *StreamWriter) PushUint64(val uint64) {
for count := 0; count < bytesPerInt64; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
v.data.WriteByte(byte(val))
v.data.WriteByte(byte(val >> 8))
v.data.WriteByte(byte(val >> 16))
v.data.WriteByte(byte(val >> 24))
}
// PushInt64 writes a uint64 qword to the stream
@ -62,7 +57,15 @@ func (v *StreamWriter) PushInt64(val int64) {
v.PushUint64(uint64(val))
}
// GetBytes returns the the byte slice of the underlying data
func (v *StreamWriter) GetBytes() []byte {
return v.data.Bytes()
// PushUint64 writes a uint64 qword to the stream
//nolint
func (v *StreamWriter) PushUint64(val uint64) {
v.data.WriteByte(byte(val))
v.data.WriteByte(byte(val >> 8))
v.data.WriteByte(byte(val >> 16))
v.data.WriteByte(byte(val >> 24))
v.data.WriteByte(byte(val >> 32))
v.data.WriteByte(byte(val >> 40))
v.data.WriteByte(byte(val >> 48))
v.data.WriteByte(byte(val >> 56))
}

View File

@ -2,7 +2,9 @@ package d2enum
// there are labels for "numeric labels (see AssetManager.TranslateLabel)
const (
CancelLabel = iota
RepairAll = iota
_
CancelLabel
CopyrightLabel
AllRightsReservedLabel
SinglePlayerLabel
@ -62,6 +64,8 @@ const (
// BaseLabelNumbers returns base label value (#n in english string table table)
func BaseLabelNumbers(idx int) int {
baseLabelNumbers := []int{
128, // repairAll
127,
// main menu labels
1612, // CANCEL
1613, // (c) 2000 Blizzard Entertainment

View File

@ -0,0 +1,11 @@
package d2enum
// SceneState enumerates the different states a scene can be in
type SceneState int
// Scene states
const (
SceneStateUninitialized SceneState = iota
SceneStateBooting
SceneStateBooted
)

View File

@ -0,0 +1,131 @@
package d2mpq
import (
"encoding/binary"
"io"
"strings"
)
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
func cryptoLookup(index uint32) uint32 {
if !cryptoBufferReady {
cryptoInitialize()
cryptoBufferReady = true
}
return cryptoBuffer[index]
}
//nolint:gomnd // Decryption magic
func cryptoInitialize() {
seed := uint32(0x00100001)
for index1 := 0; index1 < 0x100; index1++ {
index2 := index1
for i := 0; i < 5; i++ {
seed = (seed*125 + 3) % 0x2AAAAB
temp1 := (seed & 0xFFFF) << 0x10
seed = (seed*125 + 3) % 0x2AAAAB
temp2 := seed & 0xFFFF
cryptoBuffer[index2] = temp1 | temp2
index2 += 0x100
}
}
}
//nolint:gomnd // Decryption magic
func decrypt(data []uint32, seed uint32) {
seed2 := uint32(0xeeeeeeee)
for i := 0; i < len(data); i++ {
seed2 += cryptoLookup(0x400 + (seed & 0xff))
result := data[i]
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3
data[i] = result
}
}
//nolint:gomnd // Decryption magic
func decryptBytes(data []byte, seed uint32) {
seed2 := uint32(0xEEEEEEEE)
for i := 0; i < len(data)-3; i += 4 {
seed2 += cryptoLookup(0x400 + (seed & 0xFF))
result := binary.LittleEndian.Uint32(data[i : i+4])
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3
data[i+0] = uint8(result & 0xff)
data[i+1] = uint8((result >> 8) & 0xff)
data[i+2] = uint8((result >> 16) & 0xff)
data[i+3] = uint8((result >> 24) & 0xff)
}
}
//nolint:gomnd // Decryption magic
func decryptTable(r io.Reader, size uint32, name string) ([]uint32, error) {
seed := hashString(name, 3)
seed2 := uint32(0xEEEEEEEE)
size *= 4
table := make([]uint32, size)
buf := make([]byte, 4)
for i := uint32(0); i < size; i++ {
seed2 += cryptoBuffer[0x400+(seed&0xff)]
if _, err := r.Read(buf); err != nil {
return table, err
}
result := binary.LittleEndian.Uint32(buf)
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3
table[i] = result
}
return table, nil
}
func hashFilename(key string) uint64 {
a, b := hashString(key, 1), hashString(key, 2)
return uint64(a)<<32 | uint64(b)
}
//nolint:gomnd // Decryption magic
func hashString(key string, hashType uint32) uint32 {
seed1 := uint32(0x7FED7FED)
seed2 := uint32(0xEEEEEEEE)
/* prepare seeds. */
for _, char := range strings.ToUpper(key) {
seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2)
seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3
}
return seed1
}
//nolint:unused,deadcode,gomnd // will use this for creating mpq's
func encrypt(data []uint32, seed uint32) {
seed2 := uint32(0xeeeeeeee)
for i := 0; i < len(data); i++ {
seed2 += cryptoLookup(0x400 + (seed & 0xff))
result := data[i]
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = data[i] + seed2 + (seed2 << 5) + 3
data[i] = result
}
}

View File

@ -1,32 +0,0 @@
package d2mpq
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
func cryptoLookup(index uint32) uint32 {
if !cryptoBufferReady {
cryptoInitialize()
cryptoBufferReady = true
}
return cryptoBuffer[index]
}
//nolint:gomnd // magic cryptographic stuff here...
func cryptoInitialize() {
seed := uint32(0x00100001)
for index1 := 0; index1 < 0x100; index1++ {
index2 := index1
for i := 0; i < 5; i++ {
seed = (seed*125 + 3) % 0x2AAAAB
temp1 := (seed & 0xFFFF) << 0x10
seed = (seed*125 + 3) % 0x2AAAAB
temp2 := seed & 0xFFFF
cryptoBuffer[index2] = temp1 | temp2
index2 += 0x100
}
}
}

View File

@ -1,35 +0,0 @@
package d2mpq
// HashEntryMap represents a hash entry map
type HashEntryMap struct {
entries map[uint64]HashTableEntry
}
// Insert inserts a hash entry into the table
func (hem *HashEntryMap) Insert(entry *HashTableEntry) {
if hem.entries == nil {
hem.entries = make(map[uint64]HashTableEntry)
}
hem.entries[uint64(entry.NamePartA)<<32|uint64(entry.NamePartB)] = *entry
}
// Find finds a hash entry
func (hem *HashEntryMap) Find(fileName string) (*HashTableEntry, bool) {
if hem.entries == nil {
return nil, false
}
hashA := hashString(fileName, 1)
hashB := hashString(fileName, 2)
entry, found := hem.entries[uint64(hashA)<<32|uint64(hashB)]
return &entry, found
}
// Contains returns true if the hash entry contains the values
func (hem *HashEntryMap) Contains(fileName string) bool {
_, found := hem.Find(fileName)
return found
}

View File

@ -2,10 +2,9 @@ package d2mpq
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
@ -19,33 +18,11 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
// MPQ represents an MPQ archive
type MPQ struct {
filePath string
file *os.File
hashEntryMap HashEntryMap
blockTableEntries []BlockTableEntry
data Data
}
// Data Represents a MPQ file
type Data struct {
Magic [4]byte
HeaderSize uint32
ArchiveSize uint32
FormatVersion uint16
BlockSize uint16
HashTableOffset uint32
BlockTableOffset uint32
HashTableEntries uint32
BlockTableEntries uint32
}
// HashTableEntry represents a hashed file entry in the MPQ file
type HashTableEntry struct { // 16 bytes
NamePartA uint32
NamePartB uint32
Locale uint16
Platform uint16
BlockIndex uint32
filePath string
file *os.File
hashes map[uint64]*Hash
blocks []*Block
header Header
}
// PatchInfo represents patch info for the MPQ.
@ -53,71 +30,153 @@ type PatchInfo struct {
Length uint32 // Length of patch info header, in bytes
Flags uint32 // Flags. 0x80000000 = MD5 (?)
DataSize uint32 // Uncompressed size of the patch file
Md5 [16]byte // MD5 of the entire patch file after decompression
MD5 [16]byte // MD5 of the entire patch file after decompression
}
// FileFlag represents flags for a file record in the MPQ archive
type FileFlag uint32
const (
// FileImplode - File is compressed using PKWARE Data compression library
FileImplode FileFlag = 0x00000100
// FileCompress - File is compressed using combination of compression methods
FileCompress FileFlag = 0x00000200
// FileEncrypted - The file is encrypted
FileEncrypted FileFlag = 0x00010000
// FileFixKey - The decryption key for the file is altered according to the position of the file in the archive
FileFixKey FileFlag = 0x00020000
// FilePatchFile - The file contains incremental patch for an existing file in base MPQ
FilePatchFile FileFlag = 0x00100000
// FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit
FileSingleUnit FileFlag = 0x01000000
// FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch
// archives to delete files present in lower-priority archives in the search chain. The file usually
// has length of 0 or 1 byte and its name is a hash
FileDeleteMarker FileFlag = 0x02000000
// FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded.
FileSectorCrc FileFlag = 0x04000000
// FileExists - Set if file exists, reset when the file was deleted
FileExists FileFlag = 0x80000000
)
// BlockTableEntry represents an entry in the block table
type BlockTableEntry struct { // 16 bytes
FilePosition uint32
CompressedFileSize uint32
UncompressedFileSize uint32
Flags FileFlag
// Local Stuff...
FileName string
EncryptionSeed uint32
}
// HasFlag returns true if the specified flag is present
func (v BlockTableEntry) HasFlag(flag FileFlag) bool {
return (v.Flags & flag) != 0
}
// Load loads an MPQ file and returns a MPQ structure
func Load(fileName string) (d2interface.Archive, error) {
result := &MPQ{filePath: fileName}
// New loads an MPQ file and only reads the header
func New(fileName string) (*MPQ, error) {
mpq := &MPQ{filePath: fileName}
var err error
if runtime.GOOS == "linux" {
result.file, err = openIgnoreCase(fileName)
mpq.file, err = openIgnoreCase(fileName)
} else {
result.file, err = os.Open(fileName) //nolint:gosec // Will fix later
mpq.file, err = os.Open(fileName) //nolint:gosec // Will fix later
}
if err != nil {
return nil, err
}
if err := result.readHeader(); err != nil {
if err := mpq.readHeader(); err != nil {
return nil, fmt.Errorf("failed to read reader: %v", err)
}
return mpq, nil
}
// FromFile loads an MPQ file and returns a MPQ structure
func FromFile(fileName string) (*MPQ, error) {
mpq, err := New(fileName)
if err != nil {
return nil, err
}
return result, nil
if err := mpq.readHashTable(); err != nil {
return nil, fmt.Errorf("failed to read hash table: %v", err)
}
if err := mpq.readBlockTable(); err != nil {
return nil, fmt.Errorf("failed to read block table: %v", err)
}
return mpq, nil
}
// getFileBlockData gets a block table entry
func (mpq *MPQ) getFileBlockData(fileName string) (*Block, error) {
fileEntry, ok := mpq.hashes[hashFilename(fileName)]
if !ok {
return nil, errors.New("file not found")
}
if fileEntry.BlockIndex >= uint32(len(mpq.blocks)) {
return nil, errors.New("invalid block index")
}
return mpq.blocks[fileEntry.BlockIndex], nil
}
// Close closes the MPQ file
func (mpq *MPQ) Close() error {
return mpq.file.Close()
}
// ReadFile reads a file from the MPQ and returns a memory stream
func (mpq *MPQ) ReadFile(fileName string) ([]byte, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return []byte{}, err
}
fileBlockData.FileName = strings.ToLower(fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return []byte{}, err
}
buffer := make([]byte, fileBlockData.UncompressedFileSize)
if _, err := stream.Read(buffer, 0, fileBlockData.UncompressedFileSize); err != nil {
return []byte{}, err
}
return buffer, nil
}
// ReadFileStream reads the mpq file data and returns a stream
func (mpq *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return nil, err
}
fileBlockData.FileName = strings.ToLower(fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return nil, err
}
return &MpqDataStream{stream: stream}, nil
}
// ReadTextFile reads a file and returns it as a string
func (mpq *MPQ) ReadTextFile(fileName string) (string, error) {
data, err := mpq.ReadFile(fileName)
if err != nil {
return "", err
}
return string(data), nil
}
// Listfile returns the list of files in this MPQ
func (mpq *MPQ) Listfile() ([]string, error) {
data, err := mpq.ReadFile("(listfile)")
if err != nil {
return nil, err
}
raw := strings.TrimRight(string(data), "\x00")
s := bufio.NewScanner(strings.NewReader(raw))
var filePaths []string
for s.Scan() {
filePath := s.Text()
filePaths = append(filePaths, filePath)
}
return filePaths, nil
}
// Path returns the MPQ file path
func (mpq *MPQ) Path() string {
return mpq.filePath
}
// Contains returns bool for whether the given filename exists in the mpq
func (mpq *MPQ) Contains(filename string) bool {
_, ok := mpq.hashes[hashFilename(filename)]
return ok
}
// Size returns the size of the mpq in bytes
func (mpq *MPQ) Size() uint32 {
return mpq.header.ArchiveSize
}
func openIgnoreCase(mpqPath string) (*os.File, error) {
@ -142,258 +201,5 @@ func openIgnoreCase(mpqPath string) (*os.File, error) {
}
}
file, err := os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later
return file, err
}
func (v *MPQ) readHeader() error {
err := binary.Read(v.file, binary.LittleEndian, &v.data)
if err != nil {
return err
}
if string(v.data.Magic[:]) != "MPQ\x1A" {
return errors.New("invalid mpq header")
}
err = v.loadHashTable()
if err != nil {
return err
}
v.loadBlockTable()
return nil
}
func (v *MPQ) loadHashTable() error {
_, err := v.file.Seek(int64(v.data.HashTableOffset), 0)
if err != nil {
log.Panic(err)
}
hashData := make([]uint32, v.data.HashTableEntries*4) //nolint:gomnd // // Decryption magic
hash := make([]byte, 4)
for i := range hashData {
_, err := v.file.Read(hash)
if err != nil {
log.Print(err)
}
hashData[i] = binary.LittleEndian.Uint32(hash)
}
decrypt(hashData, hashString("(hash table)", 3))
for i := uint32(0); i < v.data.HashTableEntries; i++ {
v.hashEntryMap.Insert(&HashTableEntry{
NamePartA: hashData[i*4],
NamePartB: hashData[(i*4)+1],
// https://github.com/OpenDiablo2/OpenDiablo2/issues/812
Locale: uint16(hashData[(i*4)+2] >> 16), //nolint:gomnd // // binary data
Platform: uint16(hashData[(i*4)+2] & 0xFFFF), //nolint:gomnd // // binary data
BlockIndex: hashData[(i*4)+3],
})
}
return nil
}
func (v *MPQ) loadBlockTable() {
_, err := v.file.Seek(int64(v.data.BlockTableOffset), 0)
if err != nil {
log.Panic(err)
}
blockData := make([]uint32, v.data.BlockTableEntries*4) //nolint:gomnd // // binary data
hash := make([]byte, 4)
for i := range blockData {
_, err = v.file.Read(hash) //nolint:errcheck // Will fix later
if err != nil {
log.Print(err)
}
blockData[i] = binary.LittleEndian.Uint32(hash)
}
decrypt(blockData, hashString("(block table)", 3))
for i := uint32(0); i < v.data.BlockTableEntries; i++ {
v.blockTableEntries = append(v.blockTableEntries, BlockTableEntry{
FilePosition: blockData[(i * 4)],
CompressedFileSize: blockData[(i*4)+1],
UncompressedFileSize: blockData[(i*4)+2],
Flags: FileFlag(blockData[(i*4)+3]),
})
}
}
func decrypt(data []uint32, seed uint32) {
seed2 := uint32(0xeeeeeeee) //nolint:gomnd // Decryption magic
for i := 0; i < len(data); i++ {
seed2 += cryptoLookup(0x400 + (seed & 0xff)) //nolint:gomnd // Decryption magic
result := data[i]
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
data[i] = result
}
}
func decryptBytes(data []byte, seed uint32) {
seed2 := uint32(0xEEEEEEEE) //nolint:gomnd // Decryption magic
for i := 0; i < len(data)-3; i += 4 {
seed2 += cryptoLookup(0x400 + (seed & 0xFF)) //nolint:gomnd // Decryption magic
result := binary.LittleEndian.Uint32(data[i : i+4])
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
data[i+0] = uint8(result & 0xff) //nolint:gomnd // Decryption magic
data[i+1] = uint8((result >> 8) & 0xff) //nolint:gomnd // Decryption magic
data[i+2] = uint8((result >> 16) & 0xff) //nolint:gomnd // Decryption magic
data[i+3] = uint8((result >> 24) & 0xff) //nolint:gomnd // Decryption magic
}
}
func hashString(key string, hashType uint32) uint32 {
seed1 := uint32(0x7FED7FED) //nolint:gomnd // Decryption magic
seed2 := uint32(0xEEEEEEEE) //nolint:gomnd // Decryption magic
/* prepare seeds. */
for _, char := range strings.ToUpper(key) {
seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2)
seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
}
return seed1
}
// GetFileBlockData gets a block table entry
func (v *MPQ) getFileBlockData(fileName string) (BlockTableEntry, error) {
fileEntry, found := v.hashEntryMap.Find(fileName)
if !found || fileEntry.BlockIndex >= uint32(len(v.blockTableEntries)) {
return BlockTableEntry{}, errors.New("file not found")
}
return v.blockTableEntries[fileEntry.BlockIndex], nil
}
// Close closes the MPQ file
func (v *MPQ) Close() {
err := v.file.Close()
if err != nil {
log.Panic(err)
}
}
// FileExists checks the mpq to see if the file exists
func (v *MPQ) FileExists(fileName string) bool {
return v.hashEntryMap.Contains(fileName)
}
// ReadFile reads a file from the MPQ and returns a memory stream
func (v *MPQ) ReadFile(fileName string) ([]byte, error) {
fileBlockData, err := v.getFileBlockData(fileName)
if err != nil {
return []byte{}, err
}
fileBlockData.FileName = strings.ToLower(fileName)
fileBlockData.calculateEncryptionSeed()
mpqStream, err := CreateStream(v, fileBlockData, fileName)
if err != nil {
return []byte{}, err
}
buffer := make([]byte, fileBlockData.UncompressedFileSize)
mpqStream.Read(buffer, 0, fileBlockData.UncompressedFileSize)
return buffer, nil
}
// ReadFileStream reads the mpq file data and returns a stream
func (v *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := v.getFileBlockData(fileName)
if err != nil {
return nil, err
}
fileBlockData.FileName = strings.ToLower(fileName)
fileBlockData.calculateEncryptionSeed()
mpqStream, err := CreateStream(v, fileBlockData, fileName)
if err != nil {
return nil, err
}
return &MpqDataStream{stream: mpqStream}, nil
}
// ReadTextFile reads a file and returns it as a string
func (v *MPQ) ReadTextFile(fileName string) (string, error) {
data, err := v.ReadFile(fileName)
if err != nil {
return "", err
}
return string(data), nil
}
func (v *BlockTableEntry) calculateEncryptionSeed() {
fileName := path.Base(v.FileName)
v.EncryptionSeed = hashString(fileName, 3)
if !v.HasFlag(FileFixKey) {
return
}
v.EncryptionSeed = (v.EncryptionSeed + v.FilePosition) ^ v.UncompressedFileSize
}
// GetFileList returns the list of files in this MPQ
func (v *MPQ) GetFileList() ([]string, error) {
data, err := v.ReadFile("(listfile)")
if err != nil {
return nil, err
}
raw := strings.TrimRight(string(data), "\x00")
s := bufio.NewScanner(strings.NewReader(raw))
var filePaths []string
for s.Scan() {
filePath := s.Text()
filePaths = append(filePaths, filePath)
}
return filePaths, nil
}
// Path returns the MPQ file path
func (v *MPQ) Path() string {
return v.filePath
}
// Contains returns bool for whether the given filename exists in the mpq
func (v *MPQ) Contains(filename string) bool {
return v.hashEntryMap.Contains(filename)
}
// Size returns the size of the mpq in bytes
func (v *MPQ) Size() uint32 {
return v.data.ArchiveSize
return os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later
}

View File

@ -0,0 +1,77 @@
package d2mpq
import (
"io"
"strings"
)
// FileFlag represents flags for a file record in the MPQ archive
type FileFlag uint32
const (
// FileImplode - File is compressed using PKWARE Data compression library
FileImplode FileFlag = 0x00000100
// FileCompress - File is compressed using combination of compression methods
FileCompress FileFlag = 0x00000200
// FileEncrypted - The file is encrypted
FileEncrypted FileFlag = 0x00010000
// FileFixKey - The decryption key for the file is altered according to the position of the file in the archive
FileFixKey FileFlag = 0x00020000
// FilePatchFile - The file contains incremental patch for an existing file in base MPQ
FilePatchFile FileFlag = 0x00100000
// FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit
FileSingleUnit FileFlag = 0x01000000
// FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch
// archives to delete files present in lower-priority archives in the search chain. The file usually
// has length of 0 or 1 byte and its name is a hash
FileDeleteMarker FileFlag = 0x02000000
// FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded.
FileSectorCrc FileFlag = 0x04000000
// FileExists - Set if file exists, reset when the file was deleted
FileExists FileFlag = 0x80000000
)
// Block represents an entry in the block table
type Block struct { // 16 bytes
FilePosition uint32
CompressedFileSize uint32
UncompressedFileSize uint32
Flags FileFlag
// Local Stuff...
FileName string
EncryptionSeed uint32
}
// HasFlag returns true if the specified flag is present
func (b *Block) HasFlag(flag FileFlag) bool {
return (b.Flags & flag) != 0
}
func (b *Block) calculateEncryptionSeed(fileName string) {
fileName = fileName[strings.LastIndex(fileName, `\`)+1:]
seed := hashString(fileName, 3)
b.EncryptionSeed = (seed + b.FilePosition) ^ b.UncompressedFileSize
}
//nolint:gomnd // number
func (mpq *MPQ) readBlockTable() error {
if _, err := mpq.file.Seek(int64(mpq.header.BlockTableOffset), io.SeekStart); err != nil {
return err
}
blockData, err := decryptTable(mpq.file, mpq.header.BlockTableEntries, "(block table)")
if err != nil {
return err
}
for n, i := uint32(0), uint32(0); i < mpq.header.BlockTableEntries; n, i = n+4, i+1 {
mpq.blocks = append(mpq.blocks, &Block{
FilePosition: blockData[n],
CompressedFileSize: blockData[n+1],
UncompressedFileSize: blockData[n+2],
Flags: FileFlag(blockData[n+3]),
})
}
return nil
}

View File

@ -11,14 +11,14 @@ type MpqDataStream struct {
// Read reads data from the data stream
func (m *MpqDataStream) Read(p []byte) (n int, err error) {
totalRead := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), nil
totalRead, err := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), err
}
// Seek sets the position of the data stream
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
m.stream.CurrentPosition = uint32(offset + int64(whence))
return int64(m.stream.CurrentPosition), nil
m.stream.Position = uint32(offset + int64(whence))
return int64(m.stream.Position), nil
}
// Close closes the data stream

View File

@ -0,0 +1,45 @@
package d2mpq
import "io"
// Hash represents a hashed file entry in the MPQ file
type Hash struct { // 16 bytes
A uint32
B uint32
Locale uint16
Platform uint16
BlockIndex uint32
}
// Name64 returns part A and B as uint64
func (h *Hash) Name64() uint64 {
return uint64(h.A)<<32 | uint64(h.B)
}
//nolint:gomnd // number
func (mpq *MPQ) readHashTable() error {
if _, err := mpq.file.Seek(int64(mpq.header.HashTableOffset), io.SeekStart); err != nil {
return err
}
hashData, err := decryptTable(mpq.file, mpq.header.HashTableEntries, "(hash table)")
if err != nil {
return err
}
mpq.hashes = make(map[uint64]*Hash)
for n, i := uint32(0), uint32(0); i < mpq.header.HashTableEntries; n, i = n+4, i+1 {
e := &Hash{
A: hashData[n],
B: hashData[n+1],
// https://github.com/OpenDiablo2/OpenDiablo2/issues/812
Locale: uint16(hashData[n+2] >> 16), //nolint:gomnd // // binary data
Platform: uint16(hashData[n+2] & 0xFFFF), //nolint:gomnd // // binary data
BlockIndex: hashData[n+3],
}
mpq.hashes[e.Name64()] = e
}
return nil
}

View File

@ -0,0 +1,36 @@
package d2mpq
import (
"encoding/binary"
"errors"
"io"
)
// Header Represents a MPQ file
type Header struct {
Magic [4]byte
HeaderSize uint32
ArchiveSize uint32
FormatVersion uint16
BlockSize uint16
HashTableOffset uint32
BlockTableOffset uint32
HashTableEntries uint32
BlockTableEntries uint32
}
func (mpq *MPQ) readHeader() error {
if _, err := mpq.file.Seek(0, io.SeekStart); err != nil {
return err
}
if err := binary.Read(mpq.file, binary.LittleEndian, &mpq.header); err != nil {
return err
}
if string(mpq.header.Magic[:]) != "MPQ\x1A" {
return errors.New("invalid mpq header")
}
return nil
}

View File

@ -6,8 +6,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"log"
"strings"
"io"
"github.com/JoshVarga/blast"
@ -17,80 +16,63 @@ import (
// Stream represents a stream of data in an MPQ archive
type Stream struct {
BlockTableEntry BlockTableEntry
BlockPositions []uint32
CurrentData []byte
FileName string
MPQData *MPQ
EncryptionSeed uint32
CurrentPosition uint32
CurrentBlockIndex uint32
BlockSize uint32
Data []byte
Positions []uint32
MPQ *MPQ
Block *Block
Index uint32
Size uint32
Position uint32
}
// CreateStream creates an MPQ stream
func CreateStream(mpq *MPQ, blockTableEntry BlockTableEntry, fileName string) (*Stream, error) {
result := &Stream{
MPQData: mpq,
BlockTableEntry: blockTableEntry,
CurrentBlockIndex: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
}
fileSegs := strings.Split(fileName, `\`)
result.EncryptionSeed = hashString(fileSegs[len(fileSegs)-1], 3)
if result.BlockTableEntry.HasFlag(FileFixKey) {
result.EncryptionSeed = (result.EncryptionSeed + result.BlockTableEntry.FilePosition) ^ result.BlockTableEntry.UncompressedFileSize
func CreateStream(mpq *MPQ, block *Block, fileName string) (*Stream, error) {
s := &Stream{
MPQ: mpq,
Block: block,
Index: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
}
result.BlockSize = 0x200 << result.MPQData.data.BlockSize //nolint:gomnd // MPQ magic
if result.BlockTableEntry.HasFlag(FilePatchFile) {
log.Fatal("Patching is not supported")
if s.Block.HasFlag(FileFixKey) {
s.Block.calculateEncryptionSeed(fileName)
}
var err error
s.Size = 0x200 << s.MPQ.header.BlockSize //nolint:gomnd // MPQ magic
if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) &&
!result.BlockTableEntry.HasFlag(FileSingleUnit) {
err = result.loadBlockOffsets()
if s.Block.HasFlag(FilePatchFile) {
return nil, errors.New("patching is not supported")
}
return result, err
if (s.Block.HasFlag(FileCompress) || s.Block.HasFlag(FileImplode)) && !s.Block.HasFlag(FileSingleUnit) {
if err := s.loadBlockOffsets(); err != nil {
return nil, err
}
}
return s, nil
}
func (v *Stream) loadBlockOffsets() error {
blockPositionCount := ((v.BlockTableEntry.UncompressedFileSize + v.BlockSize - 1) / v.BlockSize) + 1
v.BlockPositions = make([]uint32, blockPositionCount)
_, err := v.MPQData.file.Seek(int64(v.BlockTableEntry.FilePosition), 0)
if err != nil {
if _, err := v.MPQ.file.Seek(int64(v.Block.FilePosition), io.SeekStart); err != nil {
return err
}
mpqBytes := make([]byte, blockPositionCount*4) //nolint:gomnd // MPQ magic
blockPositionCount := ((v.Block.UncompressedFileSize + v.Size - 1) / v.Size) + 1
v.Positions = make([]uint32, blockPositionCount)
_, err = v.MPQData.file.Read(mpqBytes)
if err != nil {
if err := binary.Read(v.MPQ.file, binary.LittleEndian, &v.Positions); err != nil {
return err
}
for i := range v.BlockPositions {
idx := i * 4 //nolint:gomnd // MPQ magic
v.BlockPositions[i] = binary.LittleEndian.Uint32(mpqBytes[idx : idx+4])
}
if v.Block.HasFlag(FileEncrypted) {
decrypt(v.Positions, v.Block.EncryptionSeed-1)
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
if v.BlockTableEntry.HasFlag(FileEncrypted) {
decrypt(v.BlockPositions, v.EncryptionSeed-1)
if v.BlockPositions[0] != blockPosSize {
log.Println("Decryption of MPQ failed!")
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
if v.Positions[0] != blockPosSize {
return errors.New("decryption of MPQ failed")
}
if v.BlockPositions[1] > v.BlockSize+blockPosSize {
log.Println("Decryption of MPQ failed!")
if v.Positions[1] > v.Size+blockPosSize {
return errors.New("decryption of MPQ failed")
}
}
@ -98,16 +80,18 @@ func (v *Stream) loadBlockOffsets() error {
return nil
}
func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
if v.BlockTableEntry.HasFlag(FileSingleUnit) {
func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, err error) {
if v.Block.HasFlag(FileSingleUnit) {
return v.readInternalSingleUnit(buffer, offset, count)
}
toRead := count
readTotal := uint32(0)
var read uint32
toRead := count
for toRead > 0 {
read := v.readInternal(buffer, offset, toRead)
if read, err = v.readInternal(buffer, offset, toRead); err != nil {
return 0, err
}
if read == 0 {
break
@ -118,149 +102,153 @@ func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
toRead -= read
}
return readTotal
return readTotal, nil
}
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) uint32 {
if len(v.CurrentData) == 0 {
v.loadSingleUnit()
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) (uint32, error) {
if len(v.Data) == 0 {
if err := v.loadSingleUnit(); err != nil {
return 0, err
}
}
bytesToCopy := d2math.Min(uint32(len(v.CurrentData))-v.CurrentPosition, count)
copy(buffer[offset:offset+bytesToCopy], v.CurrentData[v.CurrentPosition:v.CurrentPosition+bytesToCopy])
v.CurrentPosition += bytesToCopy
return bytesToCopy
return v.copy(buffer, offset, v.Position, count)
}
func (v *Stream) readInternal(buffer []byte, offset, count uint32) uint32 {
v.bufferData()
func (v *Stream) readInternal(buffer []byte, offset, count uint32) (uint32, error) {
if err := v.bufferData(); err != nil {
return 0, err
}
localPosition := v.CurrentPosition % v.BlockSize
bytesToCopy := d2math.MinInt32(int32(len(v.CurrentData))-int32(localPosition), int32(count))
localPosition := v.Position % v.Size
return v.copy(buffer, offset, localPosition, count)
}
func (v *Stream) copy(buffer []byte, offset, pos, count uint32) (uint32, error) {
bytesToCopy := d2math.Min(uint32(len(v.Data))-pos, count)
if bytesToCopy <= 0 {
return 0
return 0, nil
}
copy(buffer[offset:offset+uint32(bytesToCopy)], v.CurrentData[localPosition:localPosition+uint32(bytesToCopy)])
copy(buffer[offset:offset+bytesToCopy], v.Data[pos:pos+bytesToCopy])
v.Position += bytesToCopy
v.CurrentPosition += uint32(bytesToCopy)
return uint32(bytesToCopy)
return bytesToCopy, nil
}
func (v *Stream) bufferData() {
requiredBlock := v.CurrentPosition / v.BlockSize
func (v *Stream) bufferData() (err error) {
blockIndex := v.Position / v.Size
if requiredBlock == v.CurrentBlockIndex {
return
if blockIndex == v.Index {
return nil
}
expectedLength := d2math.Min(v.BlockTableEntry.UncompressedFileSize-(requiredBlock*v.BlockSize), v.BlockSize)
v.CurrentData = v.loadBlock(requiredBlock, expectedLength)
v.CurrentBlockIndex = requiredBlock
expectedLength := d2math.Min(v.Block.UncompressedFileSize-(blockIndex*v.Size), v.Size)
if v.Data, err = v.loadBlock(blockIndex, expectedLength); err != nil {
return err
}
v.Index = blockIndex
return nil
}
func (v *Stream) loadSingleUnit() {
fileData := make([]byte, v.BlockSize)
_, err := v.MPQData.file.Seek(int64(v.MPQData.data.HeaderSize), 0)
if err != nil {
log.Print(err)
func (v *Stream) loadSingleUnit() (err error) {
if _, err = v.MPQ.file.Seek(int64(v.MPQ.header.HeaderSize), io.SeekStart); err != nil {
return err
}
_, err = v.MPQData.file.Read(fileData)
if err != nil {
log.Print(err)
fileData := make([]byte, v.Size)
if _, err = v.MPQ.file.Read(fileData); err != nil {
return err
}
if v.BlockSize == v.BlockTableEntry.UncompressedFileSize {
v.CurrentData = fileData
return
if v.Size == v.Block.UncompressedFileSize {
v.Data = fileData
return nil
}
v.CurrentData = decompressMulti(fileData, v.BlockTableEntry.UncompressedFileSize)
v.Data, err = decompressMulti(fileData, v.Block.UncompressedFileSize)
return err
}
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte {
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) ([]byte, error) {
var (
offset uint32
toRead uint32
)
if v.BlockTableEntry.HasFlag(FileCompress) || v.BlockTableEntry.HasFlag(FileImplode) {
offset = v.BlockPositions[blockIndex]
toRead = v.BlockPositions[blockIndex+1] - offset
if v.Block.HasFlag(FileCompress) || v.Block.HasFlag(FileImplode) {
offset = v.Positions[blockIndex]
toRead = v.Positions[blockIndex+1] - offset
} else {
offset = blockIndex * v.BlockSize
offset = blockIndex * v.Size
toRead = expectedLength
}
offset += v.BlockTableEntry.FilePosition
offset += v.Block.FilePosition
data := make([]byte, toRead)
_, err := v.MPQData.file.Seek(int64(offset), 0)
if err != nil {
log.Print(err)
if _, err := v.MPQ.file.Seek(int64(offset), io.SeekStart); err != nil {
return []byte{}, err
}
_, err = v.MPQData.file.Read(data)
if err != nil {
log.Print(err)
if _, err := v.MPQ.file.Read(data); err != nil {
return []byte{}, err
}
if v.BlockTableEntry.HasFlag(FileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 {
if v.EncryptionSeed == 0 {
panic("Unable to determine encryption key")
if v.Block.HasFlag(FileEncrypted) && v.Block.UncompressedFileSize > 3 {
if v.Block.EncryptionSeed == 0 {
return []byte{}, errors.New("unable to determine encryption key")
}
decryptBytes(data, blockIndex+v.EncryptionSeed)
decryptBytes(data, blockIndex+v.Block.EncryptionSeed)
}
if v.BlockTableEntry.HasFlag(FileCompress) && (toRead != expectedLength) {
if !v.BlockTableEntry.HasFlag(FileSingleUnit) {
data = decompressMulti(data, expectedLength)
} else {
data = pkDecompress(data)
if v.Block.HasFlag(FileCompress) && (toRead != expectedLength) {
if !v.Block.HasFlag(FileSingleUnit) {
return decompressMulti(data, expectedLength)
}
return pkDecompress(data)
}
if v.BlockTableEntry.HasFlag(FileImplode) && (toRead != expectedLength) {
data = pkDecompress(data)
if v.Block.HasFlag(FileImplode) && (toRead != expectedLength) {
return pkDecompress(data)
}
return data
return data, nil
}
//nolint:gomnd // Will fix enum values later
func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte {
func decompressMulti(data []byte /*expectedLength*/, _ uint32) ([]byte, error) {
compressionType := data[0]
switch compressionType {
case 1: // Huffman
panic("huffman decompression not supported")
return []byte{}, errors.New("huffman decompression not supported")
case 2: // ZLib/Deflate
return deflate(data[1:])
case 8: // PKLib/Impode
return pkDecompress(data[1:])
case 0x10: // BZip2
panic("bzip2 decompression not supported")
return []byte{}, errors.New("bzip2 decompression not supported")
case 0x80: // IMA ADPCM Stereo
return d2compression.WavDecompress(data[1:], 2)
return d2compression.WavDecompress(data[1:], 2), nil
case 0x40: // IMA ADPCM Mono
return d2compression.WavDecompress(data[1:], 1)
return d2compression.WavDecompress(data[1:], 1), nil
case 0x12:
panic("lzma decompression not supported")
return []byte{}, errors.New("lzma decompression not supported")
// Combos
case 0x22:
// sparse then zlib
panic("sparse decompression + deflate decompression not supported")
return []byte{}, errors.New("sparse decompression + deflate decompression not supported")
case 0x30:
// sparse then bzip2
panic("sparse decompression + bzip2 decompression not supported")
return []byte{}, errors.New("sparse decompression + bzip2 decompression not supported")
case 0x41:
sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 1)
@ -268,69 +256,68 @@ func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte {
copy(tmp, sinput)
return tmp
return tmp, nil
case 0x48:
// byte[] result = PKDecompress(sinput, outputLength);
// return MpqWavCompression.Decompress(new MemoryStream(result), 1);
panic("pk + mpqwav decompression not supported")
return []byte{}, errors.New("pk + mpqwav decompression not supported")
case 0x81:
sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 2)
tmp := make([]byte, len(sinput))
copy(tmp, sinput)
return tmp
return tmp, nil
case 0x88:
// byte[] result = PKDecompress(sinput, outputLength);
// return MpqWavCompression.Decompress(new MemoryStream(result), 2);
panic("pk + wav decompression not supported")
default:
panic(fmt.Sprintf("decompression not supported for unknown compression type %X", compressionType))
return []byte{}, errors.New("pk + wav decompression not supported")
}
return []byte{}, fmt.Errorf("decompression not supported for unknown compression type %X", compressionType)
}
func deflate(data []byte) []byte {
func deflate(data []byte) ([]byte, error) {
b := bytes.NewReader(data)
r, err := zlib.NewReader(b)
if err != nil {
panic(err)
return []byte{}, err
}
buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(r)
if err != nil {
log.Panic(err)
return []byte{}, err
}
err = r.Close()
if err != nil {
log.Panic(err)
return []byte{}, err
}
return buffer.Bytes()
return buffer.Bytes(), nil
}
func pkDecompress(data []byte) []byte {
func pkDecompress(data []byte) ([]byte, error) {
b := bytes.NewReader(data)
r, err := blast.NewReader(b)
r, err := blast.NewReader(b)
if err != nil {
panic(err)
return []byte{}, err
}
buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(r)
if err != nil {
panic(err)
if _, err = buffer.ReadFrom(r); err != nil {
return []byte{}, err
}
err = r.Close()
if err != nil {
panic(err)
return []byte{}, err
}
return buffer.Bytes()
return buffer.Bytes(), nil
}

View File

@ -8,10 +8,9 @@ type Archive interface {
Path() string
Contains(string) bool
Size() uint32
Close()
FileExists(fileName string) bool
Close() error
ReadFile(fileName string) ([]byte, error)
ReadFileStream(fileName string) (DataStream, error)
ReadTextFile(fileName string) (string, error)
GetFileList() ([]string, error)
Listfile() ([]string, error)
}

View File

@ -1,7 +1,5 @@
package d2interface
import "github.com/hajimehoshi/ebiten/v2"
type renderCallback = func(Surface) error
type updateCallback = func() error
@ -21,7 +19,7 @@ type Renderer interface {
GetCursorPos() (int, int)
CurrentFPS() float64
ShowPanicScreen(message string)
Print(target *ebiten.Image, str string) error
PrintAt(target *ebiten.Image, str string, x, y int)
Print(target interface{}, str string) error
PrintAt(target interface{}, str string, x, y int)
GetWindowSize() (int, int)
}

View File

@ -2,11 +2,14 @@ package d2interface
import (
"github.com/gravestench/akara"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
// Scene is an extension of akara.System
type Scene interface {
akara.SystemInitializer
State() d2enum.SceneState
Key() string
Booted() bool
Paused() bool

View File

@ -13,17 +13,17 @@ type Terminal interface {
OnKeyChars(event KeyCharsEvent) bool
Render(surface Surface) error
Execute(command string) error
OutputRaw(text string, category d2enum.TermCategory)
Outputf(format string, params ...interface{})
OutputInfof(format string, params ...interface{})
OutputWarningf(format string, params ...interface{})
OutputErrorf(format string, params ...interface{})
OutputClear()
IsVisible() bool
Rawf(category d2enum.TermCategory, format string, params ...interface{})
Printf(format string, params ...interface{})
Infof(format string, params ...interface{})
Warningf(format string, params ...interface{})
Errorf(format string, params ...interface{})
Clear()
Visible() bool
Hide()
Show()
BindAction(name, description string, action interface{}) error
UnbindAction(name string) error
Bind(name, description string, arguments []string, fn func(args []string) error) error
Unbind(name ...string) error
}
// TerminalLogger is used tomake the Terminal write out

View File

@ -37,7 +37,8 @@ func Ext2SourceType(ext string) SourceType {
func CheckSourceType(path string) SourceType {
// on MacOS, the MPQ's from blizzard don't have file extensions
// so we just attempt to init the file as an mpq
if _, err := d2mpq.Load(path); err == nil {
if mpq, err := d2mpq.New(path); err == nil {
_ = mpq.Close()
return AssetSourceMPQ
}

View File

@ -14,7 +14,7 @@ var _ asset.Source = &Source{}
// NewSource creates a new MPQ Source
func NewSource(sourcePath string) (asset.Source, error) {
loaded, err := d2mpq.Load(sourcePath)
loaded, err := d2mpq.FromFile(sourcePath)
if err != nil {
return nil, err
}

View File

@ -62,7 +62,7 @@ key | value key | value
So, GetLabelModifier returns value of offset in locale languages table
*/
// some of values need to be set up. For now values with "checked" comment
// was tested and works fine in main menu.
// was tested and works fine.
func GetLabelModifier(language string) int {
modifiers := map[string]int{
"ENG": 0, // (English) // checked
@ -70,7 +70,7 @@ func GetLabelModifier(language string) int {
"DEU": 0, // (German) // checked
"FRA": 0, // (French)
"POR": 0, // (Portuguese)
"ITA": 0, // (Italian)
"ITA": 0, // (Italian) // checked
"JPN": 0, // (Japanese)
"KOR": 0, // (Korean)
"SIN": 0, //

View File

@ -193,6 +193,7 @@ const (
QuestLogQDescrBtn = "/data/global/ui/MENU/questlast.dc6"
QuestLogSocket = "/data/global/ui/MENU/questsockets.dc6"
QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6"
QuestLogDoneSfx = "cursor/questdone.wav"
// --- Mouse Pointers ---
@ -243,16 +244,18 @@ const (
MinipanelSmall = "/data/global/ui/PANEL/minipanel_s.dc6"
MinipanelButton = "/data/global/ui/PANEL/minipanelbtn.DC6"
Frame = "/data/global/ui/PANEL/800borderframe.dc6"
InventoryCharacterPanel = "/data/global/ui/PANEL/invchar6.DC6"
InventoryWeaponsTab = "/data/global/ui/PANEL/invchar6Tab.DC6"
SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_back.DC6"
SkillsPanelBarbarian = "/data/global/ui/SPELLS/skltree_b_back.DC6"
SkillsPanelDruid = "/data/global/ui/SPELLS/skltree_d_back.DC6"
SkillsPanelAssassin = "/data/global/ui/SPELLS/skltree_i_back.DC6"
SkillsPanelNecromancer = "/data/global/ui/SPELLS/skltree_n_back.DC6"
SkillsPanelPaladin = "/data/global/ui/SPELLS/skltree_p_back.DC6"
SkillsPanelSorcerer = "/data/global/ui/SPELLS/skltree_s_back.DC6"
Frame = "/data/global/ui/PANEL/800borderframe.dc6"
InventoryCharacterPanel = "/data/global/ui/PANEL/invchar6.DC6"
HeroStatsPanelStatsPoints = "/data/global/ui/PANEL/skillpoints.dc6"
HeroStatsPanelSocket = "/data/global/ui/PANEL/levelsocket.dc6"
InventoryWeaponsTab = "/data/global/ui/PANEL/invchar6Tab.DC6"
SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_back.DC6"
SkillsPanelBarbarian = "/data/global/ui/SPELLS/skltree_b_back.DC6"
SkillsPanelDruid = "/data/global/ui/SPELLS/skltree_d_back.DC6"
SkillsPanelAssassin = "/data/global/ui/SPELLS/skltree_i_back.DC6"
SkillsPanelNecromancer = "/data/global/ui/SPELLS/skltree_n_back.DC6"
SkillsPanelPaladin = "/data/global/ui/SPELLS/skltree_p_back.DC6"
SkillsPanelSorcerer = "/data/global/ui/SPELLS/skltree_s_back.DC6"
GenericSkills = "/data/global/ui/SPELLS/Skillicon.DC6"
AmazonSkills = "/data/global/ui/SPELLS/AmSkillicon.DC6"

View File

@ -332,7 +332,7 @@ func (a *Sprite) GetDirection() int {
// SetCurrentFrame sets sprite at a specific frame
func (a *Sprite) SetCurrentFrame(frameIndex int) error {
if frameIndex >= a.GetFrameCount() {
if frameIndex >= a.GetFrameCount() || frameIndex < 0 {
return errors.New("invalid frame index")
}

View File

@ -37,16 +37,16 @@ type GlyphPrinter struct {
// Basic Latin and C1 Controls and Latin-1 Supplement.
//
// DebugPrint always returns nil as of 1.5.0-alpha.
func (p *GlyphPrinter) Print(target *ebiten.Image, str string) error {
p.PrintAt(target, str, 0, 0)
func (p *GlyphPrinter) Print(target interface{}, str string) error {
p.PrintAt(target.(*ebiten.Image), str, 0, 0)
return nil
}
// PrintAt draws the string str on the image at (x, y) position.
// The available runes are in U+0000 to U+00FF, which is C0 Controls and
// Basic Latin and C1 Controls and Latin-1 Supplement.
func (p *GlyphPrinter) PrintAt(target *ebiten.Image, str string, x, y int) {
p.drawDebugText(target, str, x, y, false)
func (p *GlyphPrinter) PrintAt(target interface{}, str string, x, y int) {
p.drawDebugText(target.(*ebiten.Image), str, x, y, false)
}
func (p *GlyphPrinter) drawDebugText(target *ebiten.Image, str string, ox, oy int, shadow bool) {

View File

@ -6,6 +6,7 @@ import (
"log"
"os"
"runtime"
"sync"
)
// LogLevel determines how verbose the logging is (higher is more verbose)
@ -51,6 +52,7 @@ func NewLogger() *Logger {
l := &Logger{
level: LogLevelDefault,
colorEnabled: true,
mutex: sync.Mutex{},
}
l.Writer = log.Writer()
@ -64,6 +66,7 @@ type Logger struct {
io.Writer
level LogLevel
colorEnabled bool
mutex sync.Mutex
}
// SetPrefix sets a prefix for the message.
@ -71,11 +74,17 @@ type Logger struct {
// logger.SetPrefix("XYZ")
// logger.Debug("ABC") will print "[XYZ] [DEBUG] ABC"
func (l *Logger) SetPrefix(s string) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.prefix = s
}
// SetLevel sets the log level
func (l *Logger) SetLevel(level LogLevel) {
l.mutex.Lock()
defer l.mutex.Unlock()
if level == LogLevelUnspecified {
level = LogLevelDefault
}
@ -85,6 +94,9 @@ func (l *Logger) SetLevel(level LogLevel) {
// SetColorEnabled adds color escape-sequences to the logging output
func (l *Logger) SetColorEnabled(b bool) {
l.mutex.Lock()
defer l.mutex.Unlock()
if runtime.GOOS == "windows" {
b = false
}
@ -94,10 +106,6 @@ func (l *Logger) SetColorEnabled(b bool) {
// Info logs an info message
func (l *Logger) Info(msg string) {
if l == nil || l.level < LogLevelInfo {
return
}
go l.print(LogLevelInfo, msg)
}
@ -108,10 +116,6 @@ func (l *Logger) Infof(fmtMsg string, args ...interface{}) {
// Warning logs a warning message
func (l *Logger) Warning(msg string) {
if l == nil || l.level < LogLevelWarning {
return
}
go l.print(LogLevelWarning, msg)
}
@ -122,10 +126,6 @@ func (l *Logger) Warningf(fmtMsg string, args ...interface{}) {
// Error logs an error message
func (l *Logger) Error(msg string) {
if l == nil || l.level < LogLevelError {
return
}
go l.print(LogLevelError, msg)
}
@ -136,10 +136,6 @@ func (l *Logger) Errorf(fmtMsg string, args ...interface{}) {
// Fatal logs an fatal error message and exits programm
func (l *Logger) Fatal(msg string) {
if l == nil || l.level < LogLevelFatal {
return
}
go l.print(LogLevelFatal, msg)
os.Exit(1)
}
@ -151,10 +147,6 @@ func (l *Logger) Fatalf(fmtMsg string, args ...interface{}) {
// Debug logs a debug message
func (l *Logger) Debug(msg string) {
if l == nil || l.level < LogLevelDebug {
return
}
go l.print(LogLevelDebug, msg)
}
@ -164,7 +156,10 @@ func (l *Logger) Debugf(fmtMsg string, args ...interface{}) {
}
func (l *Logger) print(level LogLevel, msg string) {
if l == nil || l.level < level {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.level < level {
return
}

View File

@ -323,7 +323,7 @@ func (a *Animation) GetDirection() int {
// SetCurrentFrame sets animation at a specific frame
func (a *Animation) SetCurrentFrame(frameIndex int) error {
if frameIndex >= a.GetFrameCount() {
if frameIndex >= a.GetFrameCount() || frameIndex < 0 {
return errors.New("invalid frame index")
}

View File

@ -3,6 +3,7 @@ package d2asset
import (
"fmt"
"image/color"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
@ -409,43 +410,70 @@ func (am *AssetManager) loadDCC(path string,
// BindTerminalCommands binds the in-game terminal comands for the asset manager.
func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error {
if err := term.BindAction("assetspam", "display verbose asset manager logs", func(verbose bool) {
if err := term.Bind("assetspam", "display verbose asset manager logs", nil, am.commandAssetSpam(term)); err != nil {
return err
}
if err := term.Bind("assetstat", "display asset manager cache statistics", nil, am.commandAssetStat(term)); err != nil {
return err
}
if err := term.Bind("assetclear", "clear asset manager cache", nil, am.commandAssetClear); err != nil {
return err
}
return nil
}
// UnbindTerminalCommands unbinds commands from the terminal
func (am *AssetManager) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("assetspam", "assetstat", "assetclear")
}
func (am *AssetManager) commandAssetSpam(term d2interface.Terminal) func([]string) error {
return func(args []string) error {
verbose, err := strconv.ParseBool(args[0])
if err != nil {
term.Errorf("asset manager verbose invalid argument")
return nil
}
if verbose {
term.OutputInfof("asset manager verbose logging enabled")
term.Infof("asset manager verbose logging enabled")
} else {
term.OutputInfof("asset manager verbose logging disabled")
term.Infof("asset manager verbose logging disabled")
}
am.palettes.SetVerbose(verbose)
am.fonts.SetVerbose(verbose)
am.transforms.SetVerbose(verbose)
am.animations.SetVerbose(verbose)
}); err != nil {
return err
}
if err := term.BindAction("assetstat", "display asset manager cache statistics", func() {
return nil
}
}
func (am *AssetManager) commandAssetStat(term d2interface.Terminal) func([]string) error {
return func([]string) error {
var cacheStatistics = func(c d2interface.Cache) float64 {
const percent = 100.0
return float64(c.GetWeight()) / float64(c.GetBudget()) * percent
}
term.OutputInfof("palette cache: %f", cacheStatistics(am.palettes))
term.OutputInfof("palette transform cache: %f", cacheStatistics(am.transforms))
term.OutputInfof("Animation cache: %f", cacheStatistics(am.animations))
term.OutputInfof("font cache: %f", cacheStatistics(am.fonts))
}); err != nil {
return err
}
term.Infof("palette cache: %f", cacheStatistics(am.palettes))
term.Infof("palette transform cache: %f", cacheStatistics(am.transforms))
term.Infof("Animation cache: %f", cacheStatistics(am.animations))
term.Infof("font cache: %f", cacheStatistics(am.fonts))
if err := term.BindAction("assetclear", "clear asset manager cache", func() {
am.palettes.Clear()
am.transforms.Clear()
am.animations.Clear()
am.fonts.Clear()
}); err != nil {
return err
return nil
}
}
func (am *AssetManager) commandAssetClear([]string) error {
am.palettes.Clear()
am.transforms.Clear()
am.animations.Clear()
am.fonts.Clear()
return nil
}

View File

@ -9,19 +9,23 @@ import (
)
// NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly
func NewAssetManager() (*AssetManager, error) {
loader, err := d2loader.NewLoader(d2util.LogLevelDefault)
func NewAssetManager(logLevel d2util.LogLevel) (*AssetManager, error) {
loader, err := d2loader.NewLoader(logLevel)
if err != nil {
return nil, err
}
records, err := d2records.NewRecordManager(d2util.LogLevelDebug)
records, err := d2records.NewRecordManager(logLevel)
if err != nil {
return nil, err
}
logger := d2util.NewLogger()
logger.SetPrefix(logPrefix)
logger.SetLevel(logLevel)
manager := &AssetManager{
Logger: d2util.NewLogger(),
Logger: logger,
Loader: loader,
tables: make([]d2tbl.TextDictionary, 0),
animations: d2cache.CreateCache(animationBudget),
@ -31,7 +35,5 @@ func NewAssetManager() (*AssetManager, error) {
Records: records,
}
manager.SetPrefix(logPrefix)
return manager, err
}

View File

@ -2,9 +2,6 @@ package d2asset
import (
"errors"
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
@ -132,24 +129,11 @@ func (a *DCCAnimation) decodeDirection(directionIndex int) error {
func (a *DCCAnimation) decodeFrame(directionIndex int) animationFrame {
dccDirection := a.dcc.Directions[directionIndex]
minX, minY := math.MaxInt32, math.MaxInt32
maxX, maxY := math.MinInt32, math.MinInt32
for _, dccFrame := range dccDirection.Frames {
minX = d2math.MinInt(minX, dccFrame.Box.Left)
minY = d2math.MinInt(minY, dccFrame.Box.Top)
maxX = d2math.MaxInt(maxX, dccFrame.Box.Right())
maxY = d2math.MaxInt(maxY, dccFrame.Box.Bottom())
}
frameWidth := maxX - minX
frameHeight := maxY - minY
frame := animationFrame{
width: frameWidth,
height: frameHeight,
offsetX: minX,
offsetY: minY,
width: dccDirection.Box.Width,
height: dccDirection.Box.Height,
offsetX: dccDirection.Box.Left,
offsetY: dccDirection.Box.Top,
decoded: true,
}

View File

@ -3,6 +3,7 @@ package d2audio
import (
"fmt"
"math/rand"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
@ -31,7 +32,7 @@ const originalFPS float64 = 25
// A Sound that can be started and stopped
type Sound struct {
effect d2interface.SoundEffect
entry *d2records.SoundDetailsRecord
entry *d2records.SoundDetailRecord
volume float64
vTarget float64
vRate float64
@ -73,7 +74,7 @@ func (s *Sound) SetPan(pan float64) {
// Play the sound
func (s *Sound) Play() {
s.Info("starting sound" + s.entry.Handle)
s.Info("starting sound " + s.entry.Handle)
s.effect.Play()
if s.entry.FadeIn != 0 {
@ -103,6 +104,11 @@ func (s *Sound) Stop() {
}
}
// String returns the sound filename
func (s *Sound) String() string {
return s.entry.Handle
}
// SoundEngine provides functions for playing sounds
type SoundEngine struct {
asset *d2asset.AssetManager
@ -128,43 +134,25 @@ func NewSoundEngine(provider d2interface.AudioProvider,
r.Logger.SetPrefix(logPrefix)
r.Logger.SetLevel(l)
err := term.BindAction("playsoundid", "plays the sound for a given id", func(id int) {
r.PlaySoundID(id)
})
if err != nil {
if err := term.Bind("playsoundid", "plays the sound for a given id", []string{"id"}, r.commandPlaySoundID); err != nil {
r.Error(err.Error())
return nil
}
err = term.BindAction("playsound", "plays the sound for a given handle string", func(handle string) {
r.PlaySoundHandle(handle)
})
if err != nil {
if err := term.Bind("playsound", "plays the sound for a given handle string", []string{"name"}, r.commandPlaySound); err != nil {
r.Error(err.Error())
return nil
}
err = term.BindAction("activesounds", "list currently active sounds", func() {
for s := range r.sounds {
if err != nil {
r.Error(err.Error())
return
}
if err := term.Bind("activesounds", "list currently active sounds", nil, r.commandActiveSounds); err != nil {
r.Error(err.Error())
return nil
}
r.Info(fmt.Sprint(s))
}
})
err = term.BindAction("killsounds", "kill active sounds", func() {
for s := range r.sounds {
if err != nil {
r.Error(err.Error())
return
}
s.Stop()
}
})
if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil {
r.Error(err.Error())
return nil
}
return &r
}
@ -194,6 +182,11 @@ func (s *SoundEngine) Advance(elapsed float64) {
}
}
// UnbindTerminalCommands unbinds commands from the terminal
func (s *SoundEngine) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("playsoundid", "playsound", "activesounds", "killsounds")
}
// Reset stop all sounds and reset state
func (s *SoundEngine) Reset() {
for snd := range s.sounds {
@ -242,3 +235,35 @@ func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
sound := s.asset.Records.Sound.Details[handle].Index
return s.PlaySoundID(sound)
}
func (s *SoundEngine) commandPlaySoundID(args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument")
}
s.PlaySoundID(id)
return nil
}
func (s *SoundEngine) commandPlaySound(args []string) error {
s.PlaySoundHandle(args[0])
return nil
}
func (s *SoundEngine) commandActiveSounds([]string) error {
for sound := range s.sounds {
s.Info(sound.String())
}
return nil
}
func (s *SoundEngine) commandKillSounds([]string) error {
for sound := range s.sounds {
sound.Stop()
}
return nil
}

View File

@ -13,7 +13,8 @@ type CommandRegistration struct {
Enabled bool
Name string
Description string
Callback interface{}
Args []string
Callback func(args []string) error
}
// New creates a new CommandRegistration. By default, IsCommandRegistration is false.

View File

@ -5,8 +5,6 @@ import (
"os"
"path"
"path/filepath"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
// Configuration defines the configuration for the engine, loaded from config.json
@ -21,7 +19,6 @@ type Configuration struct {
RunInBackground bool
VsyncEnabled bool
Backend string
LogLevel d2util.LogLevel
path string
}

View File

@ -4,8 +4,6 @@ import (
"os/user"
"path"
"runtime"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
// DefaultConfig creates and returns a default configuration
@ -37,8 +35,7 @@ func DefaultConfig() *Configuration {
"d2video.mpq",
"d2speech.mpq",
},
LogLevel: d2util.LogLevelDefault,
path: DefaultConfigPath(),
path: DefaultConfigPath(),
}
switch runtime.GOOS {

View File

@ -74,8 +74,7 @@ func NewBox(
renderer d2interface.Renderer,
ui *d2ui.UIManager,
contentLayout *Layout,
width, height int,
x, y int,
width, height, x, y int,
l d2util.LogLevel,
title string,
) *Box {

View File

@ -1,8 +1,6 @@
package d2gui
import (
"image/color"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
@ -37,28 +35,3 @@ func renderSegmented(animation d2interface.Animation, segmentsX, segmentsY, fram
func half(n int) int {
return n / 2
}
func rgbaColor(rgba uint32) color.RGBA {
result := color.RGBA{}
a, b, g, r := 0, 1, 2, 3
byteWidth := 8
byteMask := 0xff
for idx := 0; idx < 4; idx++ {
shift := idx * byteWidth
component := uint8(rgba>>shift) & uint8(byteMask)
switch idx {
case a:
result.A = component
case b:
result.B = component
case g:
result.G = component
case r:
result.R = component
}
}
return result
}

View File

@ -248,16 +248,16 @@ func (l *Layout) renderEntryDebug(entry *layoutEntry, target d2interface.Surface
target.PushTranslation(entry.x, entry.y)
defer target.Pop()
drawColor := rgbaColor(white)
drawColor := d2util.Color(white)
switch entry.widget.(type) {
case *Layout:
drawColor = rgbaColor(magenta)
drawColor = d2util.Color(magenta)
case *SpacerStatic, *SpacerDynamic:
drawColor = rgbaColor(grey2)
drawColor = d2util.Color(grey2)
case *Label:
drawColor = rgbaColor(green)
drawColor = d2util.Color(green)
case *Button:
drawColor = rgbaColor(yellow)
drawColor = d2util.Color(yellow)
}
target.DrawLine(entry.width, 0, drawColor)
@ -487,7 +487,7 @@ func (l *Layout) createButton(renderer d2interface.Renderer, text string,
return nil, loadErr
}
textColor := rgbaColor(grey)
textColor := d2util.Color(grey)
textWidth, textHeight := font.GetTextMetrics(text)
textX := half(buttonWidth) - half(textWidth)
textY := half(buttonHeight) - half(textHeight) + config.textOffset

View File

@ -64,10 +64,7 @@ const (
)
// NewLayoutScrollbar attaches a scrollbar to the parentLayout to control the targetLayout
func NewLayoutScrollbar(
parentLayout *Layout,
targetLayout *Layout,
) *LayoutScrollbar {
func NewLayoutScrollbar(parentLayout, targetLayout *Layout) *LayoutScrollbar {
parentW, parentH := parentLayout.GetSize()
_, targetH := targetLayout.GetSize()
gutterHeight := parentH - (2 * textSliderPartHeight)

View File

@ -110,7 +110,7 @@ func (f *HeroStateFactory) GetAllHeroStates() ([]*HeroState, error) {
}
// CreateHeroSkillsState will assemble the hero skills from the class stats record.
func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatsRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) {
func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) {
baseSkills := map[int]*HeroSkill{}
for idx := range classStats.BaseSkill {

View File

@ -10,32 +10,27 @@ type HeroStatsState struct {
Level int `json:"level"`
Experience int `json:"experience"`
Vitality int `json:"vitality"`
Energy int `json:"energy"`
Strength int `json:"strength"`
Energy int `json:"energy"`
Dexterity int `json:"dexterity"`
Vitality int `json:"vitality"`
// there are stats and skills points remaining to add.
StatsPoints int `json:"statsPoints"`
SkillPoints int `json:"skillPoints"`
AttackRating int `json:"attackRating"`
DefenseRating int `json:"defenseRating"`
MaxStamina int `json:"maxStamina"`
Health int `json:"health"`
MaxHealth int `json:"maxHealth"`
Mana int `json:"mana"`
MaxMana int `json:"maxMana"`
FireResistance int `json:"fireResistance"`
ColdResistance int `json:"coldResistance"`
LightningResistance int `json:"lightningResistance"`
PoisonResistance int `json:"poisonResistance"`
Health int `json:"health"`
MaxHealth int `json:"maxHealth"`
Mana int `json:"mana"`
MaxMana int `json:"maxMana"`
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
MaxStamina int `json:"maxStamina"`
// values which are not saved/loaded(computed)
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
NextLevelExp int `json:"-"`
NextLevelExp int `json:"-"`
}
// CreateHeroStatsState generates a running state from a hero stats.
func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2records.CharStatsRecord) *HeroStatsState {
func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2records.CharStatRecord) *HeroStatsState {
result := HeroStatsState{
Level: 1,
Experience: 0,
@ -44,6 +39,8 @@ func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStat
Dexterity: classStats.InitDex,
Vitality: classStats.InitVit,
Energy: classStats.InitEne,
StatsPoints: 0,
SkillPoints: 0,
MaxHealth: classStats.InitVit * classStats.LifePerVit,
MaxMana: classStats.InitEne * classStats.ManaPerEne,

View File

@ -136,7 +136,6 @@ func (f *InventoryItemFactory) GetMiscItemByCode(code string) (*InventoryItemMis
// GetWeaponItemByCode returns the weapon item for the given code
func (f *InventoryItemFactory) GetWeaponItemByCode(code string) (*InventoryItemWeapon, error) {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/796
result := f.asset.Records.Item.Weapons[code]
if result == nil {
return nil, fmt.Errorf("could not find weapon entry for code '%s'", code)

View File

@ -277,7 +277,7 @@ var itemStatCosts = map[string]*d2records.ItemStatCostRecord{
}
// nolint:gochecknoglobals // just a test
var charStats = map[d2enum.Hero]*d2records.CharStatsRecord{
var charStats = map[d2enum.Hero]*d2records.CharStatRecord{
d2enum.HeroPaladin: {
Class: d2enum.HeroPaladin,
SkillStrAll: "to Paladin Skill Levels",
@ -297,7 +297,7 @@ var skillDetails = map[int]*d2records.SkillRecord{
}
// nolint:gochecknoglobals // just a test
var monStats = map[string]*d2records.MonStatsRecord{
var monStats = map[string]*d2records.MonStatRecord{
"Specter": {NameString: "Specter", ID: 40},
}

View File

@ -256,7 +256,7 @@ func (m *MapEngine) RemoveEntity(entity d2interface.MapEntity) {
// GetTiles returns a slice of all tiles matching the given style,
// sequence and tileType.
func (m *MapEngine) GetTiles(style, sequence int, tileType d2enum.TileType) []d2dt1.Tile {
tiles := make([]d2dt1.Tile, 0, len(m.dt1TileData))
tiles := make([]d2dt1.Tile, 0)
for idx := range m.dt1TileData {
if m.dt1TileData[idx].Style != int32(style) || m.dt1TileData[idx].Sequence != int32(sequence) ||

View File

@ -64,7 +64,7 @@ func NewAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEntit
// NewPlayer creates a new player entity and returns a pointer to it.
func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero,
stats *d2hero.HeroStatsState, skills map[int]*d2hero.HeroSkill, equipment *d2inventory.CharacterEquipment,
leftSkill, rightSkill int, gold int) *Player {
leftSkill, rightSkill, gold int) *Player {
layerEquipment := &[d2enum.CompositeTypeMax]string{
d2enum.CompositeTypeHead: equipment.Head.GetArmorClass(),
d2enum.CompositeTypeTorso: equipment.Torso.GetArmorClass(),
@ -180,7 +180,7 @@ func (f *MapEntityFactory) NewItem(x, y int, codes ...string) (*Item, error) {
}
// NewNPC creates a new NPC and returns a pointer to it.
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatsRecord, direction int) (*NPC, error) {
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatRecord, direction int) (*NPC, error) {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/803
result := &NPC{
mapEntity: newMapEntity(x, y),
@ -237,7 +237,6 @@ func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.Ove
return nil, err
}
// https://github.com/OpenDiablo2/OpenDiablo2/issues/767
animation.Rewind()
animation.ResetPlayedCount()
@ -263,7 +262,7 @@ func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.Ove
}
// NewObject creates an instance of AnimatedComposite
func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailsRecord,
func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailRecord,
palettePath string) (*Object, error) {
locX, locY := float64(x), float64(y)
entity := &Object{

View File

@ -22,8 +22,8 @@ type NPC struct {
action int
path int
repetitions int
monstatRecord *d2records.MonStatsRecord
monstatEx *d2records.MonStats2Record
monstatRecord *d2records.MonStatRecord
monstatEx *d2records.MonStat2Record
HasPaths bool
isDone bool
}

View File

@ -20,7 +20,7 @@ type Object struct {
composite *d2asset.Composite
highlight bool
// nameLabel d2ui.Label
objectRecord *d2records.ObjectDetailsRecord
objectRecord *d2records.ObjectDetailRecord
drawLayer int
name string
}

View File

@ -2,8 +2,10 @@ package d2maprenderer
import (
"errors"
"fmt"
"image/color"
"math"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
@ -86,20 +88,11 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
result.Camera.position = &startPosition
result.viewport.SetCamera(&result.Camera)
var err error
err = term.BindAction("mapdebugvis", "set map debug visualization level", func(level int) {
result.mapDebugVisLevel = level
})
if err != nil {
if err := term.Bind("mapdebugvis", "set map debug visualization level", nil, result.commandMapDebugVis); err != nil {
result.Errorf("could not bind the mapdebugvis action, err: %v", err)
}
err = term.BindAction("entitydebugvis", "set entity debug visualization level", func(level int) {
result.entityDebugVisLevel = level
})
if err != nil {
if err := term.Bind("entitydebugvis", "set entity debug visualization level", nil, result.commandEntityDebugVis); err != nil {
result.Errorf("could not bind the entitydebugvis action, err: %v", err)
}
@ -110,6 +103,33 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
return result
}
// UnbindTerminalCommands unbinds commands from the terminal
func (mr *MapRenderer) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("mapdebugvis", "entitydebugvis")
}
func (mr *MapRenderer) commandMapDebugVis(args []string) error {
level, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument supplied")
}
mr.mapDebugVisLevel = level
return nil
}
func (mr *MapRenderer) commandEntityDebugVis(args []string) error {
level, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument supplied")
}
mr.entityDebugVisLevel = level
return nil
}
// RegenerateTileCache calls MapRenderer.generateTileCache().
func (mr *MapRenderer) RegenerateTileCache() {
mr.generateTileCache()

View File

@ -22,7 +22,7 @@ func armorTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Animation.Token.Armor = records
r.Logger.Infof("Loaded %d ArmorType records", len(records))
r.Debugf("Loaded %d ArmorType records", len(records))
return nil
}

View File

@ -79,7 +79,7 @@ func autoMagicLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d AutoMagic records", len(records))
r.Debugf("Loaded %d AutoMagic records", len(records))
r.Item.AutoMagic = records

View File

@ -37,7 +37,7 @@ func autoMapLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d AutoMapRecord records", len(records))
r.Debugf("Loaded %d AutoMap records", len(records))
r.Level.AutoMaps = records

View File

@ -102,7 +102,7 @@ func beltsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d belts", len(records))
r.Debugf("Loaded %d Belt records", len(records))
r.Item.Belts = records

View File

@ -19,7 +19,7 @@ func bodyLocationsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
panic(d.Err)
}
r.Logger.Infof("Loaded %d Body Location records", len(records))
r.Debugf("Loaded %d BodyLocation records", len(records))
r.BodyLocations = records

View File

@ -8,7 +8,7 @@ func booksLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(Books)
for d.Next() {
record := &BooksRecord{
record := &BookRecord{
Name: d.String("Name"),
Namco: d.String("Namco"),
Completed: d.String("Completed"),
@ -28,7 +28,7 @@ func booksLoader(r *RecordManager, d *d2txt.DataDictionary) error {
panic(d.Err)
}
r.Logger.Infof("Loaded %d book items", len(records))
r.Debugf("Loaded %d Book records", len(records))
r.Item.Books = records

View File

@ -1,10 +1,10 @@
package d2records
// Books stores all of the BooksRecords
type Books map[string]*BooksRecord
// Books stores all of the BookRecords
type Books map[string]*BookRecord
// BooksRecord is a representation of a row from books.txt
type BooksRecord struct {
// BookRecord is a representation of a row from books.txt
type BookRecord struct {
Name string
Namco string // The displayed name, where the string prefix is "Tome"
Completed string

View File

@ -5,32 +5,28 @@ import (
)
func skillCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records, err := loadCalculations(r, d)
records, err := loadCalculations(r, d, "Skill")
if err != nil {
return err
}
r.Logger.Infof("Loaded %d Skill Calculation records", len(records))
r.Calculation.Skills = records
return nil
}
func missileCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records, err := loadCalculations(r, d)
records, err := loadCalculations(r, d, "Missile")
if err != nil {
return err
}
r.Logger.Infof("Loaded %d Missile Calculation records", len(records))
r.Calculation.Missiles = records
return nil
}
func loadCalculations(r *RecordManager, d *d2txt.DataDictionary) (Calculations, error) {
func loadCalculations(r *RecordManager, d *d2txt.DataDictionary, name string) (Calculations, error) {
records := make(Calculations)
for d.Next() {
@ -45,7 +41,7 @@ func loadCalculations(r *RecordManager, d *d2txt.DataDictionary) (Calculations,
return nil, d.Err
}
r.Logger.Infof("Loaded %d Skill Calculation records", len(records))
r.Debugf("Loaded %d %s Calculation records", len(records), name)
return records, nil
}

View File

@ -38,7 +38,7 @@ func charStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
}
for d.Next() {
record := &CharStatsRecord{
record := &CharStatRecord{
Class: stringMap[d.String("class")],
InitStr: d.Number("str"),
@ -136,7 +136,7 @@ func charStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d CharStats records", len(records))
r.Debugf("Loaded %d CharStat records", len(records))
r.Character.Stats = records

View File

@ -2,11 +2,11 @@ package d2records
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// CharStats holds all of the CharStatsRecords
type CharStats map[d2enum.Hero]*CharStatsRecord
// CharStats holds all of the CharStatRecords
type CharStats map[d2enum.Hero]*CharStatRecord
// CharStatsRecord is a struct that represents a single row from charstats.txt
type CharStatsRecord struct {
// CharStatRecord is a struct that represents a single row from charstats.txt
type CharStatRecord struct {
Class d2enum.Hero
// the initial stats at character level 1

View File

@ -22,7 +22,7 @@ func colorsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Colors = records
r.Logger.Infof("Loaded %d Color records", len(records))
r.Debugf("Loaded %d Color records", len(records))
return nil
}

View File

@ -19,7 +19,7 @@ func componentCodesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d ComponentCode records", len(records))
r.Debugf("Loaded %d ComponentCode records", len(records))
r.ComponentCodes = records

View File

@ -22,7 +22,7 @@ func compositeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Animation.Token.Composite = records
r.Logger.Infof("Loaded %d Composite Type records", len(records))
r.Debugf("Loaded %d CompositeType records", len(records))
return nil
}

View File

@ -22,7 +22,7 @@ func cubeModifierLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.Cube.Modifiers = records
r.Logger.Infof("Loaded %d Cube Modifier records", len(records))
r.Debugf("Loaded %d CubeModifier records", len(records))
return nil
}

View File

@ -22,7 +22,7 @@ func cubeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.Cube.Types = records
r.Logger.Infof("Loaded %d Cube Type records", len(records))
r.Debugf("Loaded %d CubeType records", len(records))
return nil
}

View File

@ -96,7 +96,7 @@ func cubeRecipeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d CubeMainRecord records", len(records))
r.Debugf("Loaded %d CubeRecipe records", len(records))
r.Item.Cube.Recipes = records

View File

@ -42,7 +42,7 @@ func difficultyLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d DifficultyLevel records", len(records))
r.Debugf("Loaded %d DifficultyLevel records", len(records))
r.DifficultyLevels = records

View File

@ -20,7 +20,7 @@ func elemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d ElemType records", len(records))
r.Debugf("Loaded %d ElemType records", len(records))
r.ElemTypes = records

View File

@ -20,7 +20,7 @@ func eventsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d Event records", len(records))
r.Debugf("Loaded %d Event records", len(records))
r.Character.Events = records

View File

@ -48,7 +48,7 @@ func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
}
for d.Next() {
record := &ExperienceBreakpointsRecord{
record := &ExperienceBreakpointRecord{
Level: d.Number("Level"),
HeroBreakpoints: map[d2enum.Hero]int{
d2enum.HeroAmazon: d.Number("Amazon"),
@ -68,7 +68,7 @@ func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d Experience Breakpoint records", len(breakpoints))
r.Debugf("Loaded %d ExperienceBreakpoint records", len(breakpoints))
r.Character.MaxLevel = maxLevels
r.Character.Experience = breakpoints

View File

@ -4,14 +4,14 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// ExperienceBreakpoints describes the required experience
// for each level for each character class
type ExperienceBreakpoints map[int]*ExperienceBreakpointsRecord
type ExperienceBreakpoints map[int]*ExperienceBreakpointRecord
// ExperienceMaxLevels defines the max character levels
type ExperienceMaxLevels map[d2enum.Hero]int
// ExperienceBreakpointsRecord describes the experience points required to
// ExperienceBreakpointRecord describes the experience points required to
// gain a level for all character classes
type ExperienceBreakpointsRecord struct {
type ExperienceBreakpointRecord struct {
Level int
HeroBreakpoints map[d2enum.Hero]int
Ratio int

View File

@ -19,7 +19,7 @@ func gambleLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d gamble records", len(records))
r.Debugf("Loaded %d Gamble records", len(records))
r.Gamble = records

View File

@ -4,12 +4,12 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// LoadGems loads gem records into a map[string]*GemsRecord
// LoadGems loads gem records into a map[string]*GemRecord
func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(Gems)
for d.Next() {
gem := &GemsRecord{
gem := &GemRecord{
Name: d.String("name"),
Letter: d.String("letter"),
Transform: d.Number("transform"),
@ -60,7 +60,7 @@ func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d Gems records", len(records))
r.Debugf("Loaded %d Gem records", len(records))
r.Item.Gems = records

View File

@ -1,11 +1,11 @@
package d2records
// Gems stores all of the GemsRecords
type Gems map[string]*GemsRecord
// Gems stores all of the GemRecords
type Gems map[string]*GemRecord
// GemsRecord is a representation of a single row of gems.txt
// GemRecord is a representation of a single row of gems.txt
// it describes the properties of socketable items
type GemsRecord struct {
type GemRecord struct {
Name string
Letter string
Transform int

View File

@ -22,7 +22,7 @@ func hirelingDescriptionLoader(r *RecordManager, d *d2txt.DataDictionary) error
r.Hireling.Descriptions = records
r.Logger.Infof("Loaded %d Hireling Descriptions records", len(records))
r.Debugf("Loaded %d HirelingDescription records", len(records))
return nil
}

View File

@ -90,7 +90,7 @@ func hirelingLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d Hireling records", len(records))
r.Debugf("Loaded %d Hireling records", len(records))
r.Hireling.Details = records

View File

@ -22,7 +22,7 @@ func hitClassLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Animation.Token.HitClass = records
r.Logger.Infof("Loaded %d HitClass records", len(records))
r.Debugf("Loaded %d HitClass records", len(records))
return nil
}

View File

@ -130,7 +130,7 @@ func inventoryLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d Inventory Panel records", len(records))
r.Debugf("Loaded %d Inventory records", len(records))
r.Layout.Inventory = records

View File

@ -70,7 +70,7 @@ func loadAffixDictionary(
}
name := getAffixString(superType, subType)
r.Logger.Infof("Loaded %d %s records", len(records), name)
r.Debugf("Loaded %d %s records", len(records), name)
return records, groups, nil
}

View File

@ -16,7 +16,7 @@ func armorLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return err
}
r.Logger.Infof("Loaded %d armors", len(records))
r.Debugf("Loaded %d Armor Item records", len(records))
r.Item.Armors = records

View File

@ -21,7 +21,7 @@ func lowQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.LowQualityPrefixes = records
r.Logger.Infof("Loaded %d Low Item Quality records", len(records))
r.Debugf("Loaded %d LowQuality records", len(records))
return nil
}

View File

@ -13,7 +13,7 @@ func miscItemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return err
}
r.Logger.Infof("Loaded %d misc items", len(records))
r.Debugf("Loaded %d Misc Item records", len(records))
r.Item.Misc = records

View File

@ -45,7 +45,7 @@ func itemQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.Quality = records
r.Logger.Infof("Loaded %d ItemQualities records", len(records))
r.Debugf("Loaded %d ItemQuality records", len(records))
return nil
}

View File

@ -55,7 +55,7 @@ func itemRatioLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d ItemRatio records", len(records))
r.Debugf("Loaded %d ItemRatio records", len(records))
r.Item.Ratios = records

View File

@ -76,7 +76,7 @@ func itemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d ItemType records", len(records))
r.Debugf("Loaded %d ItemType records", len(records))
r.Item.Types = records
r.Item.Equivalency = equivMap

View File

@ -13,7 +13,7 @@ func weaponsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return err
}
r.Logger.Infof("Loaded %d weapons", len(records))
r.Debugf("Loaded %d Weapon records", len(records))
r.Item.Weapons = records

View File

@ -95,7 +95,7 @@ func itemStatCostLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d ItemStatCost records", len(records))
r.Debugf("Loaded %d ItemStatCost records", len(records))
r.Item.Stats = records

View File

@ -11,7 +11,7 @@ func levelDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(LevelDetails)
for d.Next() {
record := &LevelDetailsRecord{
record := &LevelDetailRecord{
Name: d.String("Name "),
ID: d.Number("Id"),
Palette: d.Number("Pal"),
@ -165,7 +165,7 @@ func levelDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d LevelDetails records", len(records))
r.Debugf("Loaded %d LevelDetail records", len(records))
r.Level.Details = records

View File

@ -2,13 +2,13 @@ package d2records
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// LevelDetails has all of the LevelDetailsRecords
type LevelDetails map[int]*LevelDetailsRecord
// LevelDetails has all of the LevelDetailRecords
type LevelDetails map[int]*LevelDetailRecord
// LevelDetailsRecord is a representation of a row from levels.txt
// LevelDetailRecord is a representation of a row from levels.txt
// it describes lots of things about the levels, like where they are connected,
// what kinds of monsters spawn, the level generator type, and lots of other stuff.
type LevelDetailsRecord struct {
type LevelDetailRecord struct {
// Name
// This column has no function, it only serves as a comment field to make it

View File

@ -8,7 +8,7 @@ func levelMazeDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(LevelMazeDetails)
for d.Next() {
record := &LevelMazeDetailsRecord{
record := &LevelMazeDetailRecord{
Name: d.String("Name"),
LevelID: d.Number("Level"),
NumRoomsNormal: d.Number("Rooms"),
@ -24,7 +24,7 @@ func levelMazeDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d LevelMazeDetails records", len(records))
r.Debugf("Loaded %d LevelMazeDetail records", len(records))
r.Level.Maze = records

View File

@ -1,11 +1,11 @@
package d2records
// LevelMazeDetails stores all of the LevelMazeDetailsRecords
type LevelMazeDetails map[int]*LevelMazeDetailsRecord
// LevelMazeDetails stores all of the LevelMazeDetailRecords
type LevelMazeDetails map[int]*LevelMazeDetailRecord
// LevelMazeDetailsRecord is a representation of a row from lvlmaze.txt
// LevelMazeDetailRecord is a representation of a row from lvlmaze.txt
// these records define the parameters passed to the maze level generator
type LevelMazeDetailsRecord struct {
type LevelMazeDetailRecord struct {
// descriptive, not loaded in game. Corresponds with Name field in
// Levels.txt
Name string // Name

View File

@ -42,7 +42,7 @@ func levelPresetLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records[record.DefinitionID] = record
}
r.Logger.Infof("Loaded %d level presets", len(records))
r.Debugf("Loaded %d LevelPresets records", len(records))
if d.Err != nil {
return d.Err

View File

@ -40,7 +40,7 @@ func levelSubstitutionsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d LevelSubstitution records", len(records))
r.Debugf("Loaded %d LevelSubstitution records", len(records))
r.Level.Sub = records

View File

@ -58,7 +58,7 @@ func levelTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d LevelType records", len(records))
r.Debugf("Loaded %d LevelType records", len(records))
r.Level.Types = records

View File

@ -30,7 +30,7 @@ func levelWarpsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d level warps", len(records))
r.Debugf("Loaded %d LevelWarp records", len(records))
r.Level.Warp = records

View File

@ -304,7 +304,7 @@ func missilesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d Missile Records", len(records))
r.Debugf("Loaded %d Missile records", len(records))
r.Missiles = records

View File

@ -19,7 +19,7 @@ func monsterAiLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d MonsterAI records", len(records))
r.Debugf("Loaded %d MonsterAI records", len(records))
r.Monster.AI = records

View File

@ -49,7 +49,7 @@ func monsterEquipmentLoader(r *RecordManager, d *d2txt.DataDictionary) error {
length += len(records[k])
}
r.Logger.Infof("Loaded %d MonsterEquipment records", length)
r.Debugf("Loaded %d MonsterEquipment records", length)
r.Monster.Equipment = records

View File

@ -52,7 +52,7 @@ func monsterLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d MonsterLevel records", len(records))
r.Debugf("Loaded %d MonsterLevel records", len(records))
r.Monster.Levels = records

View File

@ -21,7 +21,7 @@ func monsterModeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Logger.Infof("Loaded %d MonMode records", len(records))
r.Debugf("Loaded %d MonMode records", len(records))
r.Monster.Modes = records

View File

@ -1,6 +1,6 @@
package d2records
// MonModes stores all of the GemsRecords
// MonModes stores all of the MonModeRecords
type MonModes map[string]*MonModeRecord
// MonModeRecord is a representation of a single row of Monmode.txt

Some files were not shown because too many files have changed in this diff Show More