diff --git a/.github/workflows/pullRequest.yml b/.github/workflows/pullRequest.yml index f208b494..471307b2 100644 --- a/.github/workflows/pullRequest.yml +++ b/.github/workflows/pullRequest.yml @@ -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 diff --git a/.github/workflows/pushToMaster.yml b/.github/workflows/pushToMaster.yml deleted file mode 100644 index 346cc565..00000000 --- a/.github/workflows/pushToMaster.yml +++ /dev/null @@ -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 . diff --git a/README.md b/README.md index 1b143564..3a103eff 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/d2app/app.go b/d2app/app.go index 99110921..afdf3ced 100644 --- a/d2app/app.go +++ b/d2app/app.go @@ -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 []\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)) } diff --git a/d2common/d2datautils/stream_reader.go b/d2common/d2datautils/stream_reader.go index c66d8212..31e06c96 100644 --- a/d2common/d2datautils/stream_reader.go +++ b/d2common/d2datautils/stream_reader.go @@ -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 diff --git a/d2common/d2datautils/stream_writer.go b/d2common/d2datautils/stream_writer.go index 10870a73..849cbdb2 100644 --- a/d2common/d2datautils/stream_writer.go +++ b/d2common/d2datautils/stream_writer.go @@ -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)) } diff --git a/d2common/d2enum/numeric_labels.go b/d2common/d2enum/numeric_labels.go index 6383b6e5..eb2253b6 100644 --- a/d2common/d2enum/numeric_labels.go +++ b/d2common/d2enum/numeric_labels.go @@ -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 diff --git a/d2common/d2enum/scene_state.go b/d2common/d2enum/scene_state.go new file mode 100644 index 00000000..83d08634 --- /dev/null +++ b/d2common/d2enum/scene_state.go @@ -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 +) diff --git a/d2common/d2fileformats/d2mpq/crypto.go b/d2common/d2fileformats/d2mpq/crypto.go new file mode 100644 index 00000000..636c8bc2 --- /dev/null +++ b/d2common/d2fileformats/d2mpq/crypto.go @@ -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 + } +} diff --git a/d2common/d2fileformats/d2mpq/crypto_buff.go b/d2common/d2fileformats/d2mpq/crypto_buff.go deleted file mode 100644 index 7618743b..00000000 --- a/d2common/d2fileformats/d2mpq/crypto_buff.go +++ /dev/null @@ -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 - } - } -} diff --git a/d2common/d2fileformats/d2mpq/hash_entry_map.go b/d2common/d2fileformats/d2mpq/hash_entry_map.go deleted file mode 100644 index ab9c0ca1..00000000 --- a/d2common/d2fileformats/d2mpq/hash_entry_map.go +++ /dev/null @@ -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 -} diff --git a/d2common/d2fileformats/d2mpq/mpq.go b/d2common/d2fileformats/d2mpq/mpq.go index 23deed54..59f3a8b1 100644 --- a/d2common/d2fileformats/d2mpq/mpq.go +++ b/d2common/d2fileformats/d2mpq/mpq.go @@ -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 } diff --git a/d2common/d2fileformats/d2mpq/mpq_block.go b/d2common/d2fileformats/d2mpq/mpq_block.go new file mode 100644 index 00000000..112e0e89 --- /dev/null +++ b/d2common/d2fileformats/d2mpq/mpq_block.go @@ -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 +} diff --git a/d2common/d2fileformats/d2mpq/mpq_data_stream.go b/d2common/d2fileformats/d2mpq/mpq_data_stream.go index db66260c..3a92064a 100644 --- a/d2common/d2fileformats/d2mpq/mpq_data_stream.go +++ b/d2common/d2fileformats/d2mpq/mpq_data_stream.go @@ -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 diff --git a/d2common/d2fileformats/d2mpq/mpq_hash.go b/d2common/d2fileformats/d2mpq/mpq_hash.go new file mode 100644 index 00000000..3f1f744a --- /dev/null +++ b/d2common/d2fileformats/d2mpq/mpq_hash.go @@ -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 +} diff --git a/d2common/d2fileformats/d2mpq/mpq_header.go b/d2common/d2fileformats/d2mpq/mpq_header.go new file mode 100644 index 00000000..f27cfaf7 --- /dev/null +++ b/d2common/d2fileformats/d2mpq/mpq_header.go @@ -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 +} diff --git a/d2common/d2fileformats/d2mpq/mpq_stream.go b/d2common/d2fileformats/d2mpq/mpq_stream.go index b6156322..5be4951d 100644 --- a/d2common/d2fileformats/d2mpq/mpq_stream.go +++ b/d2common/d2fileformats/d2mpq/mpq_stream.go @@ -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 } diff --git a/d2common/d2interface/archive.go b/d2common/d2interface/archive.go index 5e9d60da..9de0f9ea 100644 --- a/d2common/d2interface/archive.go +++ b/d2common/d2interface/archive.go @@ -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) } diff --git a/d2common/d2interface/renderer.go b/d2common/d2interface/renderer.go index b5b23142..b2fd2716 100644 --- a/d2common/d2interface/renderer.go +++ b/d2common/d2interface/renderer.go @@ -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) } diff --git a/d2common/d2interface/scene.go b/d2common/d2interface/scene.go index 72f3e2f8..facffcec 100644 --- a/d2common/d2interface/scene.go +++ b/d2common/d2interface/scene.go @@ -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 diff --git a/d2common/d2interface/terminal.go b/d2common/d2interface/terminal.go index c6496437..be3bd31c 100644 --- a/d2common/d2interface/terminal.go +++ b/d2common/d2interface/terminal.go @@ -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 diff --git a/d2common/d2loader/asset/types/source_types.go b/d2common/d2loader/asset/types/source_types.go index 563c3a7b..32a2b402 100644 --- a/d2common/d2loader/asset/types/source_types.go +++ b/d2common/d2loader/asset/types/source_types.go @@ -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 } diff --git a/d2common/d2loader/mpq/source.go b/d2common/d2loader/mpq/source.go index 4a326a42..d5b4236d 100644 --- a/d2common/d2loader/mpq/source.go +++ b/d2common/d2loader/mpq/source.go @@ -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 } diff --git a/d2common/d2resource/languages_map.go b/d2common/d2resource/languages_map.go index 44045a17..511058f8 100644 --- a/d2common/d2resource/languages_map.go +++ b/d2common/d2resource/languages_map.go @@ -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, // diff --git a/d2common/d2resource/resource_paths.go b/d2common/d2resource/resource_paths.go index a18a196f..bbd88dd7 100644 --- a/d2common/d2resource/resource_paths.go +++ b/d2common/d2resource/resource_paths.go @@ -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" diff --git a/d2common/d2sprite/sprite.go b/d2common/d2sprite/sprite.go index 4593923c..2ff8438f 100644 --- a/d2common/d2sprite/sprite.go +++ b/d2common/d2sprite/sprite.go @@ -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") } diff --git a/d2common/d2util/debug_print.go b/d2common/d2util/debug_print.go index daa20386..489f28ae 100644 --- a/d2common/d2util/debug_print.go +++ b/d2common/d2util/debug_print.go @@ -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) { diff --git a/d2common/d2util/logger.go b/d2common/d2util/logger.go index 0ed3fd21..00e60db0 100644 --- a/d2common/d2util/logger.go +++ b/d2common/d2util/logger.go @@ -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 } diff --git a/d2core/d2asset/animation.go b/d2core/d2asset/animation.go index 94fe94af..b03aae64 100644 --- a/d2core/d2asset/animation.go +++ b/d2core/d2asset/animation.go @@ -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") } diff --git a/d2core/d2asset/asset_manager.go b/d2core/d2asset/asset_manager.go index 0146170f..359befa0 100644 --- a/d2core/d2asset/asset_manager.go +++ b/d2core/d2asset/asset_manager.go @@ -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 } diff --git a/d2core/d2asset/d2asset.go b/d2core/d2asset/d2asset.go index 9abd6605..0e721554 100644 --- a/d2core/d2asset/d2asset.go +++ b/d2core/d2asset/d2asset.go @@ -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 } diff --git a/d2core/d2asset/dcc_animation.go b/d2core/d2asset/dcc_animation.go index 9961b5ea..326f8124 100644 --- a/d2core/d2asset/dcc_animation.go +++ b/d2core/d2asset/dcc_animation.go @@ -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, } diff --git a/d2core/d2audio/sound_engine.go b/d2core/d2audio/sound_engine.go index f811c1f8..4ef91f57 100644 --- a/d2core/d2audio/sound_engine.go +++ b/d2core/d2audio/sound_engine.go @@ -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 +} diff --git a/d2core/d2components/command_registration.go b/d2core/d2components/command_registration.go index 28016cf3..e3ee72f0 100644 --- a/d2core/d2components/command_registration.go +++ b/d2core/d2components/command_registration.go @@ -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. diff --git a/d2core/d2config/d2config.go b/d2core/d2config/d2config.go index 3d57e753..de17fc27 100644 --- a/d2core/d2config/d2config.go +++ b/d2core/d2config/d2config.go @@ -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 } diff --git a/d2core/d2config/defaults.go b/d2core/d2config/defaults.go index a717005d..7e8a539b 100644 --- a/d2core/d2config/defaults.go +++ b/d2core/d2config/defaults.go @@ -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 { diff --git a/d2core/d2gui/box.go b/d2core/d2gui/box.go index 446d5e10..63f42394 100644 --- a/d2core/d2gui/box.go +++ b/d2core/d2gui/box.go @@ -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 { diff --git a/d2core/d2gui/common.go b/d2core/d2gui/common.go index f81bb4e6..02dd2094 100644 --- a/d2core/d2gui/common.go +++ b/d2core/d2gui/common.go @@ -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 -} diff --git a/d2core/d2gui/layout.go b/d2core/d2gui/layout.go index 43125acf..44c7f7a0 100644 --- a/d2core/d2gui/layout.go +++ b/d2core/d2gui/layout.go @@ -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 diff --git a/d2core/d2gui/layout_scrollbar.go b/d2core/d2gui/layout_scrollbar.go index 151a2a34..38bf2551 100644 --- a/d2core/d2gui/layout_scrollbar.go +++ b/d2core/d2gui/layout_scrollbar.go @@ -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) diff --git a/d2core/d2hero/hero_state_factory.go b/d2core/d2hero/hero_state_factory.go index 43ba461e..10ad1bd2 100644 --- a/d2core/d2hero/hero_state_factory.go +++ b/d2core/d2hero/hero_state_factory.go @@ -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 { diff --git a/d2core/d2hero/hero_stats_state.go b/d2core/d2hero/hero_stats_state.go index 04ad52a4..4a3f1e99 100644 --- a/d2core/d2hero/hero_stats_state.go +++ b/d2core/d2hero/hero_stats_state.go @@ -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, diff --git a/d2core/d2inventory/inventory_item_factory.go b/d2core/d2inventory/inventory_item_factory.go index fb081fcc..7cf1d273 100644 --- a/d2core/d2inventory/inventory_item_factory.go +++ b/d2core/d2inventory/inventory_item_factory.go @@ -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) diff --git a/d2core/d2item/diablo2item/item_property_test.go b/d2core/d2item/diablo2item/item_property_test.go index ec932ff5..bb98f697 100644 --- a/d2core/d2item/diablo2item/item_property_test.go +++ b/d2core/d2item/diablo2item/item_property_test.go @@ -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}, } diff --git a/d2core/d2map/d2mapengine/engine.go b/d2core/d2map/d2mapengine/engine.go index 4ab2e17f..97ae289d 100644 --- a/d2core/d2map/d2mapengine/engine.go +++ b/d2core/d2map/d2mapengine/engine.go @@ -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) || diff --git a/d2core/d2map/d2mapentity/factory.go b/d2core/d2map/d2mapentity/factory.go index 3d42c753..181b05c2 100644 --- a/d2core/d2map/d2mapentity/factory.go +++ b/d2core/d2map/d2mapentity/factory.go @@ -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{ diff --git a/d2core/d2map/d2mapentity/npc.go b/d2core/d2map/d2mapentity/npc.go index 663401e0..66284092 100644 --- a/d2core/d2map/d2mapentity/npc.go +++ b/d2core/d2map/d2mapentity/npc.go @@ -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 } diff --git a/d2core/d2map/d2mapentity/object.go b/d2core/d2map/d2mapentity/object.go index 71dbaf1e..314e9e04 100644 --- a/d2core/d2map/d2mapentity/object.go +++ b/d2core/d2map/d2mapentity/object.go @@ -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 } diff --git a/d2core/d2map/d2maprenderer/renderer.go b/d2core/d2map/d2maprenderer/renderer.go index ab0be25a..e4abd367 100644 --- a/d2core/d2map/d2maprenderer/renderer.go +++ b/d2core/d2map/d2maprenderer/renderer.go @@ -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() diff --git a/d2core/d2records/armor_type_loader.go b/d2core/d2records/armor_type_loader.go index 612e0481..30a2b0fc 100644 --- a/d2core/d2records/armor_type_loader.go +++ b/d2core/d2records/armor_type_loader.go @@ -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 } diff --git a/d2core/d2records/automagic_loader.go b/d2core/d2records/automagic_loader.go index b666c96a..cb66cef8 100644 --- a/d2core/d2records/automagic_loader.go +++ b/d2core/d2records/automagic_loader.go @@ -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 diff --git a/d2core/d2records/automap_loader.go b/d2core/d2records/automap_loader.go index fe33002c..69f26ad5 100644 --- a/d2core/d2records/automap_loader.go +++ b/d2core/d2records/automap_loader.go @@ -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 diff --git a/d2core/d2records/belts_loader.go b/d2core/d2records/belts_loader.go index ba954718..622f8dec 100644 --- a/d2core/d2records/belts_loader.go +++ b/d2core/d2records/belts_loader.go @@ -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 diff --git a/d2core/d2records/body_locations_loader.go b/d2core/d2records/body_locations_loader.go index 70c9855e..6b660c74 100644 --- a/d2core/d2records/body_locations_loader.go +++ b/d2core/d2records/body_locations_loader.go @@ -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 diff --git a/d2core/d2records/books_loader.go b/d2core/d2records/books_loader.go index 9b8c46fa..866f3228 100644 --- a/d2core/d2records/books_loader.go +++ b/d2core/d2records/books_loader.go @@ -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 diff --git a/d2core/d2records/books_record.go b/d2core/d2records/books_record.go index 119185d7..472179ea 100644 --- a/d2core/d2records/books_record.go +++ b/d2core/d2records/books_record.go @@ -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 diff --git a/d2core/d2records/calculations_loader.go b/d2core/d2records/calculations_loader.go index bc3330cb..3a07677f 100644 --- a/d2core/d2records/calculations_loader.go +++ b/d2core/d2records/calculations_loader.go @@ -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 } diff --git a/d2core/d2records/charstats_loader.go b/d2core/d2records/charstats_loader.go index b5af365d..86f1d30a 100644 --- a/d2core/d2records/charstats_loader.go +++ b/d2core/d2records/charstats_loader.go @@ -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 diff --git a/d2core/d2records/charstats_record.go b/d2core/d2records/charstats_record.go index 5d7bd35d..c7717ac1 100644 --- a/d2core/d2records/charstats_record.go +++ b/d2core/d2records/charstats_record.go @@ -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 diff --git a/d2core/d2records/color_loader.go b/d2core/d2records/color_loader.go index 1f2c502a..2adb67cc 100644 --- a/d2core/d2records/color_loader.go +++ b/d2core/d2records/color_loader.go @@ -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 } diff --git a/d2core/d2records/component_codes_loader.go b/d2core/d2records/component_codes_loader.go index 6a840f4b..af02f09d 100644 --- a/d2core/d2records/component_codes_loader.go +++ b/d2core/d2records/component_codes_loader.go @@ -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 diff --git a/d2core/d2records/composite_type_loader.go b/d2core/d2records/composite_type_loader.go index c60ed58c..ecb5c4ea 100644 --- a/d2core/d2records/composite_type_loader.go +++ b/d2core/d2records/composite_type_loader.go @@ -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 } diff --git a/d2core/d2records/cube_modifier_loader.go b/d2core/d2records/cube_modifier_loader.go index f9ddc7fa..18e25847 100644 --- a/d2core/d2records/cube_modifier_loader.go +++ b/d2core/d2records/cube_modifier_loader.go @@ -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 } diff --git a/d2core/d2records/cube_type_loader.go b/d2core/d2records/cube_type_loader.go index 18bbf171..b79551f6 100644 --- a/d2core/d2records/cube_type_loader.go +++ b/d2core/d2records/cube_type_loader.go @@ -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 } diff --git a/d2core/d2records/cubemain_loader.go b/d2core/d2records/cubemain_loader.go index 76692521..01d61e40 100644 --- a/d2core/d2records/cubemain_loader.go +++ b/d2core/d2records/cubemain_loader.go @@ -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 diff --git a/d2core/d2records/difficultylevels_loader.go b/d2core/d2records/difficultylevels_loader.go index c880e1e5..c048b2cd 100644 --- a/d2core/d2records/difficultylevels_loader.go +++ b/d2core/d2records/difficultylevels_loader.go @@ -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 diff --git a/d2core/d2records/elemtype_loader.go b/d2core/d2records/elemtype_loader.go index 7142a0ac..863dd712 100644 --- a/d2core/d2records/elemtype_loader.go +++ b/d2core/d2records/elemtype_loader.go @@ -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 diff --git a/d2core/d2records/events_loader.go b/d2core/d2records/events_loader.go index 08db2485..4af88bbc 100644 --- a/d2core/d2records/events_loader.go +++ b/d2core/d2records/events_loader.go @@ -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 diff --git a/d2core/d2records/experience_loader.go b/d2core/d2records/experience_loader.go index 7380420f..5ef9b922 100644 --- a/d2core/d2records/experience_loader.go +++ b/d2core/d2records/experience_loader.go @@ -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 diff --git a/d2core/d2records/experience_record.go b/d2core/d2records/experience_record.go index 9b80b0e4..821122a5 100644 --- a/d2core/d2records/experience_record.go +++ b/d2core/d2records/experience_record.go @@ -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 diff --git a/d2core/d2records/gamble_loader.go b/d2core/d2records/gamble_loader.go index dda7ac08..7fce2967 100644 --- a/d2core/d2records/gamble_loader.go +++ b/d2core/d2records/gamble_loader.go @@ -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 diff --git a/d2core/d2records/gems_loader.go b/d2core/d2records/gems_loader.go index bc71b23a..5db90827 100644 --- a/d2core/d2records/gems_loader.go +++ b/d2core/d2records/gems_loader.go @@ -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 diff --git a/d2core/d2records/gems_record.go b/d2core/d2records/gems_record.go index 62879a00..cbb69e76 100644 --- a/d2core/d2records/gems_record.go +++ b/d2core/d2records/gems_record.go @@ -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 diff --git a/d2core/d2records/hireling_description_loader.go b/d2core/d2records/hireling_description_loader.go index fa6c7d0c..e289f9d2 100644 --- a/d2core/d2records/hireling_description_loader.go +++ b/d2core/d2records/hireling_description_loader.go @@ -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 } diff --git a/d2core/d2records/hireling_loader.go b/d2core/d2records/hireling_loader.go index c1262988..785d7f76 100644 --- a/d2core/d2records/hireling_loader.go +++ b/d2core/d2records/hireling_loader.go @@ -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 diff --git a/d2core/d2records/hit_class_loader.go b/d2core/d2records/hit_class_loader.go index 0c265066..68c98d47 100644 --- a/d2core/d2records/hit_class_loader.go +++ b/d2core/d2records/hit_class_loader.go @@ -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 } diff --git a/d2core/d2records/inventory_loader.go b/d2core/d2records/inventory_loader.go index cbe0974c..931c81fe 100644 --- a/d2core/d2records/inventory_loader.go +++ b/d2core/d2records/inventory_loader.go @@ -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 diff --git a/d2core/d2records/item_affix_loader.go b/d2core/d2records/item_affix_loader.go index 2b0f7999..09f1608d 100644 --- a/d2core/d2records/item_affix_loader.go +++ b/d2core/d2records/item_affix_loader.go @@ -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 } diff --git a/d2core/d2records/item_armor_loader.go b/d2core/d2records/item_armor_loader.go index 2023f7fd..d910e964 100644 --- a/d2core/d2records/item_armor_loader.go +++ b/d2core/d2records/item_armor_loader.go @@ -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 diff --git a/d2core/d2records/item_low_quality_loader.go b/d2core/d2records/item_low_quality_loader.go index eb8b99bf..ec491780 100644 --- a/d2core/d2records/item_low_quality_loader.go +++ b/d2core/d2records/item_low_quality_loader.go @@ -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 } diff --git a/d2core/d2records/item_misc_loader.go b/d2core/d2records/item_misc_loader.go index 0851fd08..957abeaa 100644 --- a/d2core/d2records/item_misc_loader.go +++ b/d2core/d2records/item_misc_loader.go @@ -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 diff --git a/d2core/d2records/item_quality_loader.go b/d2core/d2records/item_quality_loader.go index f3d92e74..e497376d 100644 --- a/d2core/d2records/item_quality_loader.go +++ b/d2core/d2records/item_quality_loader.go @@ -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 } diff --git a/d2core/d2records/item_ratio_loader.go b/d2core/d2records/item_ratio_loader.go index 06e460b7..bc47202f 100644 --- a/d2core/d2records/item_ratio_loader.go +++ b/d2core/d2records/item_ratio_loader.go @@ -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 diff --git a/d2core/d2records/item_types_loader.go b/d2core/d2records/item_types_loader.go index 258791f3..e278e0b6 100644 --- a/d2core/d2records/item_types_loader.go +++ b/d2core/d2records/item_types_loader.go @@ -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 diff --git a/d2core/d2records/item_weapons_loader.go b/d2core/d2records/item_weapons_loader.go index dfd9e2cd..cbc12d56 100644 --- a/d2core/d2records/item_weapons_loader.go +++ b/d2core/d2records/item_weapons_loader.go @@ -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 diff --git a/d2core/d2records/itemstatcost_loader.go b/d2core/d2records/itemstatcost_loader.go index ed4c328a..e4f38f76 100644 --- a/d2core/d2records/itemstatcost_loader.go +++ b/d2core/d2records/itemstatcost_loader.go @@ -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 diff --git a/d2core/d2records/level_details_loader.go b/d2core/d2records/level_details_loader.go index e53722ff..5591523b 100644 --- a/d2core/d2records/level_details_loader.go +++ b/d2core/d2records/level_details_loader.go @@ -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 diff --git a/d2core/d2records/level_details_record.go b/d2core/d2records/level_details_record.go index db9430b2..386882e5 100644 --- a/d2core/d2records/level_details_record.go +++ b/d2core/d2records/level_details_record.go @@ -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 diff --git a/d2core/d2records/level_maze_loader.go b/d2core/d2records/level_maze_loader.go index 8c592922..0b6edd6a 100644 --- a/d2core/d2records/level_maze_loader.go +++ b/d2core/d2records/level_maze_loader.go @@ -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 diff --git a/d2core/d2records/level_maze_record.go b/d2core/d2records/level_maze_record.go index 61877e5b..b47dfee3 100644 --- a/d2core/d2records/level_maze_record.go +++ b/d2core/d2records/level_maze_record.go @@ -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 diff --git a/d2core/d2records/level_presets_loader.go b/d2core/d2records/level_presets_loader.go index 8cd32ea6..39847d6b 100644 --- a/d2core/d2records/level_presets_loader.go +++ b/d2core/d2records/level_presets_loader.go @@ -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 diff --git a/d2core/d2records/level_substitutions_loader.go b/d2core/d2records/level_substitutions_loader.go index 3eacde9c..0b0059a8 100644 --- a/d2core/d2records/level_substitutions_loader.go +++ b/d2core/d2records/level_substitutions_loader.go @@ -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 diff --git a/d2core/d2records/level_types_loader.go b/d2core/d2records/level_types_loader.go index 160a621c..8ff4cabd 100644 --- a/d2core/d2records/level_types_loader.go +++ b/d2core/d2records/level_types_loader.go @@ -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 diff --git a/d2core/d2records/level_warp_loader.go b/d2core/d2records/level_warp_loader.go index f474f94a..84bc6964 100644 --- a/d2core/d2records/level_warp_loader.go +++ b/d2core/d2records/level_warp_loader.go @@ -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 diff --git a/d2core/d2records/missiles_loader.go b/d2core/d2records/missiles_loader.go index 6183c2fb..807e88e5 100644 --- a/d2core/d2records/missiles_loader.go +++ b/d2core/d2records/missiles_loader.go @@ -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 diff --git a/d2core/d2records/monster_ai_loader.go b/d2core/d2records/monster_ai_loader.go index 5f89565e..47841eea 100644 --- a/d2core/d2records/monster_ai_loader.go +++ b/d2core/d2records/monster_ai_loader.go @@ -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 diff --git a/d2core/d2records/monster_equipment_loader.go b/d2core/d2records/monster_equipment_loader.go index 5639649a..0e4b800a 100644 --- a/d2core/d2records/monster_equipment_loader.go +++ b/d2core/d2records/monster_equipment_loader.go @@ -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 diff --git a/d2core/d2records/monster_levels_record.go b/d2core/d2records/monster_levels_record.go index 89bb71f9..6b71e601 100644 --- a/d2core/d2records/monster_levels_record.go +++ b/d2core/d2records/monster_levels_record.go @@ -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 diff --git a/d2core/d2records/monster_mode_loader.go b/d2core/d2records/monster_mode_loader.go index 23e512fb..41cb4adf 100644 --- a/d2core/d2records/monster_mode_loader.go +++ b/d2core/d2records/monster_mode_loader.go @@ -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 diff --git a/d2core/d2records/monster_mode_record.go b/d2core/d2records/monster_mode_record.go index 7a36ff05..9df79421 100644 --- a/d2core/d2records/monster_mode_record.go +++ b/d2core/d2records/monster_mode_record.go @@ -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 diff --git a/d2core/d2records/monster_placement_loader.go b/d2core/d2records/monster_placement_loader.go index a1e56df0..adfbb8e4 100644 --- a/d2core/d2records/monster_placement_loader.go +++ b/d2core/d2records/monster_placement_loader.go @@ -18,7 +18,7 @@ func monsterPlacementsLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Monster.Placements = records - r.Logger.Infof("Loaded %d MonsterPlacement records", len(records)) + r.Debugf("Loaded %d MonsterPlacement records", len(records)) return nil } diff --git a/d2core/d2records/monster_preset_loader.go b/d2core/d2records/monster_preset_loader.go index c2e84ea3..d9380549 100644 --- a/d2core/d2records/monster_preset_loader.go +++ b/d2core/d2records/monster_preset_loader.go @@ -21,7 +21,7 @@ func monsterPresetLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d MonPreset records", len(records)) + r.Debugf("Loaded %d MonPreset records", len(records)) r.Monster.Presets = records diff --git a/d2core/d2records/monster_property_loader.go b/d2core/d2records/monster_property_loader.go index 0662c40d..b3e63c3c 100644 --- a/d2core/d2records/monster_property_loader.go +++ b/d2core/d2records/monster_property_loader.go @@ -57,7 +57,7 @@ func monsterPropertiesLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d MonProp records", len(records)) + r.Debugf("Loaded %d MonProp records", len(records)) r.Monster.Props = records diff --git a/d2core/d2records/monster_sequence_loader.go b/d2core/d2records/monster_sequence_loader.go index 449c6b3e..e168fe1d 100644 --- a/d2core/d2records/monster_sequence_loader.go +++ b/d2core/d2records/monster_sequence_loader.go @@ -31,7 +31,7 @@ func monsterSequencesLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d MonsterSequence records", len(records)) + r.Debugf("Loaded %d MonsterSequence records", len(records)) r.Monster.Sequences = records diff --git a/d2core/d2records/monster_sound_loader.go b/d2core/d2records/monster_sound_loader.go index 96b6909c..3f8aa3f2 100644 --- a/d2core/d2records/monster_sound_loader.go +++ b/d2core/d2records/monster_sound_loader.go @@ -57,7 +57,7 @@ func monsterSoundsLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d Monster Sound records", len(records)) + r.Debugf("Loaded %d MonsterSound records", len(records)) r.Monster.Sounds = records diff --git a/d2core/d2records/monster_stats2_loader.go b/d2core/d2records/monster_stats2_loader.go index 51981d27..91fc7ca0 100644 --- a/d2core/d2records/monster_stats2_loader.go +++ b/d2core/d2records/monster_stats2_loader.go @@ -7,7 +7,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt" ) -// LoadMonStats2 loads MonStats2Records from monstats2.txt +// LoadMonStats2 loads MonStat2Records from monstats2.txt //nolint:funlen //just a big data loader func monsterStats2Loader(r *RecordManager, d *d2txt.DataDictionary) error { records := make(MonStats2) @@ -18,7 +18,7 @@ func monsterStats2Loader(r *RecordManager, d *d2txt.DataDictionary) error { return err } - record := &MonStats2Record{ + record := &MonStat2Record{ Key: d.String("Id"), Height: d.Number("Height"), OverlayHeight: d.Number("OverlayHeight"), @@ -161,7 +161,7 @@ func monsterStats2Loader(r *RecordManager, d *d2txt.DataDictionary) error { panic(d.Err) } - r.Logger.Infof("Loaded %d MonStats2 records", len(records)) + r.Debugf("Loaded %d MonStat2 records", len(records)) r.Monster.Stats2 = records diff --git a/d2core/d2records/monster_stats2_record.go b/d2core/d2records/monster_stats2_record.go index 306ea1e4..914b1c61 100644 --- a/d2core/d2records/monster_stats2_record.go +++ b/d2core/d2records/monster_stats2_record.go @@ -2,11 +2,11 @@ package d2records import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" -// MonStats2 stores all of the MonStats2Records -type MonStats2 map[string]*MonStats2Record +// MonStats2 stores all of the MonStat2Records +type MonStats2 map[string]*MonStat2Record -// MonStats2Record is a representation of a row from monstats2.txt -type MonStats2Record struct { +// MonStat2Record is a representation of a row from monstats2.txt +type MonStat2Record struct { // Available options for equipment // randomly selected from EquipmentOptions [16][]string diff --git a/d2core/d2records/monster_stats_loader.go b/d2core/d2records/monster_stats_loader.go index a6b8c6a9..e9675623 100644 --- a/d2core/d2records/monster_stats_loader.go +++ b/d2core/d2records/monster_stats_loader.go @@ -10,7 +10,7 @@ func monsterStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error { records := make(MonStats) for d.Next() { - record := &MonStatsRecord{ + record := &MonStatRecord{ Key: d.String("Id"), ID: d.Number("hcIdx"), BaseKey: d.String("BaseId"), @@ -272,7 +272,7 @@ func monsterStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d MonStats records", len(records)) + r.Debugf("Loaded %d MonStat records", len(records)) r.Monster.Stats = records diff --git a/d2core/d2records/monster_stats_record.go b/d2core/d2records/monster_stats_record.go index 2469299a..191bb62c 100644 --- a/d2core/d2records/monster_stats_record.go +++ b/d2core/d2records/monster_stats_record.go @@ -4,13 +4,13 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" // https://d2mods.info/forum/kb/viewarticle?a=360 -// MonStats stores all of the MonStat Records -type MonStats map[string]*MonStatsRecord +// MonStats stores all of the MonStatRecords +type MonStats map[string]*MonStatRecord type ( - // MonStatsRecord represents a single row from `data/global/excel/monstats.txt` in the MPQ files. + // MonStatRecord represents a single row from `data/global/excel/monstats.txt` in the MPQ files. // These records are used for creating monsters. - MonStatsRecord struct { + MonStatRecord struct { // Key contains the pointer that will be used in other txt files // such as levels.txt and superuniques.txt. diff --git a/d2core/d2records/monster_super_unique_loader.go b/d2core/d2records/monster_super_unique_loader.go index 774bb77a..852c0f25 100644 --- a/d2core/d2records/monster_super_unique_loader.go +++ b/d2core/d2records/monster_super_unique_loader.go @@ -40,7 +40,7 @@ func monsterSuperUniqeLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Monster.Unique.Super = records - r.Logger.Infof("Loaded %d SuperUnique records", len(records)) + r.Debugf("Loaded %d SuperUnique records", len(records)) return nil } diff --git a/d2core/d2records/monster_type_loader.go b/d2core/d2records/monster_type_loader.go index 75f2fd0d..d4635af9 100644 --- a/d2core/d2records/monster_type_loader.go +++ b/d2core/d2records/monster_type_loader.go @@ -23,7 +23,7 @@ func monsterTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error { panic(d.Err) } - r.Logger.Infof("Loaded %d MonType records", len(records)) + r.Debugf("Loaded %d MonType records", len(records)) r.Monster.Types = records diff --git a/d2core/d2records/monster_unique_affix_loader.go b/d2core/d2records/monster_unique_affix_loader.go index cfe6f497..0061926a 100644 --- a/d2core/d2records/monster_unique_affix_loader.go +++ b/d2core/d2records/monster_unique_affix_loader.go @@ -59,7 +59,7 @@ func uniqueMonsterPrefixLoader(r *RecordManager, d *d2txt.DataDictionary) error r.Monster.Name.Prefix = records - r.Logger.Infof("Loaded %d unique monster prefix records", len(records)) + r.Debugf("Loaded %d UniqueMonsterPrefix records", len(records)) return nil } @@ -72,7 +72,7 @@ func uniqueMonsterSuffixLoader(r *RecordManager, d *d2txt.DataDictionary) error r.Monster.Name.Suffix = records - r.Logger.Infof("Loaded %d unique monster suffix records", len(records)) + r.Debugf("Loaded %d UniqueMonsterSuffix records", len(records)) return nil } diff --git a/d2core/d2records/monster_unique_modifiers_loader.go b/d2core/d2records/monster_unique_modifiers_loader.go index 949d0ef4..3da4aec3 100644 --- a/d2core/d2records/monster_unique_modifiers_loader.go +++ b/d2core/d2records/monster_unique_modifiers_loader.go @@ -48,7 +48,7 @@ func monsterUniqModifiersLoader(r *RecordManager, d *d2txt.DataDictionary) error return d.Err } - r.Logger.Infof("Loaded %d MonsterUniqueModifier records", len(records)) + r.Debugf("Loaded %d MonsterUniqueModifier records", len(records)) r.Monster.Unique.Mods = records r.Monster.Unique.Constants = constants diff --git a/d2core/d2records/npc_loader.go b/d2core/d2records/npc_loader.go index 685a3a5c..ebf9a590 100644 --- a/d2core/d2records/npc_loader.go +++ b/d2core/d2records/npc_loader.go @@ -64,7 +64,7 @@ func npcLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.NPCs = records - r.Logger.Infof("Loaded %d NPC records", len(records)) + r.Debugf("Loaded %d NPC records", len(records)) return nil } diff --git a/d2core/d2records/object_details_loader.go b/d2core/d2records/object_details_loader.go index c7dca4c6..2dcbf43d 100644 --- a/d2core/d2records/object_details_loader.go +++ b/d2core/d2records/object_details_loader.go @@ -11,7 +11,7 @@ func objectDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error { i := 0 for d.Next() { - record := &ObjectDetailsRecord{ + record := &ObjectDetailRecord{ Index: i, Name: d.String("Name"), Description: d.String("description - not loaded"), @@ -225,7 +225,7 @@ func objectDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d objects", len(records)) + r.Debugf("Loaded %d ObjectDetail records", len(records)) r.Object.Details = records diff --git a/d2core/d2records/object_details_record.go b/d2core/d2records/object_details_record.go index ee56249a..7137cf63 100644 --- a/d2core/d2records/object_details_record.go +++ b/d2core/d2records/object_details_record.go @@ -1,10 +1,10 @@ package d2records // ObjectDetails stores all of the ObjectDetailRecords -type ObjectDetails map[int]*ObjectDetailsRecord +type ObjectDetails map[int]*ObjectDetailRecord -// ObjectDetailsRecord represents the settings for one type of object from objects.txt -type ObjectDetailsRecord struct { +// ObjectDetailRecord represents the settings for one type of object from objects.txt +type ObjectDetailRecord struct { Index int // Line number in file, this is the actual index used for objects FrameCount [8]int // how many frames does this mode have, 0 = skip FrameDelta [8]int // what rate is the animation played at (256 = 100% speed) diff --git a/d2core/d2records/object_groups_loader.go b/d2core/d2records/object_groups_loader.go index 8a4ee9c7..ed109cfd 100644 --- a/d2core/d2records/object_groups_loader.go +++ b/d2core/d2records/object_groups_loader.go @@ -32,7 +32,7 @@ func objectGroupsLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d ObjectGroup records", len(records)) + r.Debugf("Loaded %d ObjectGroup records", len(records)) return nil } diff --git a/d2core/d2records/object_mode_loader.go b/d2core/d2records/object_mode_loader.go index dc544360..39c40ae2 100644 --- a/d2core/d2records/object_mode_loader.go +++ b/d2core/d2records/object_mode_loader.go @@ -22,7 +22,7 @@ func objectModesLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Object.Modes = records - r.Logger.Infof("Loaded %d ObjectMode records", len(records)) + r.Debugf("Loaded %d ObjectMode records", len(records)) return nil } diff --git a/d2core/d2records/object_types_loader.go b/d2core/d2records/object_types_loader.go index fdb9f7bd..aa855a96 100644 --- a/d2core/d2records/object_types_loader.go +++ b/d2core/d2records/object_types_loader.go @@ -23,7 +23,7 @@ func objectTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d object types", len(records)) + r.Debugf("Loaded %d ObjectType records", len(records)) r.Object.Types = records diff --git a/d2core/d2records/overlays_loader.go b/d2core/d2records/overlays_loader.go index aed7a028..a10344ab 100644 --- a/d2core/d2records/overlays_loader.go +++ b/d2core/d2records/overlays_loader.go @@ -35,7 +35,7 @@ func overlaysLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d Overlay records", len(records)) + r.Debugf("Loaded %d Overlay records", len(records)) r.Layout.Overlays = records diff --git a/d2core/d2records/pet_type_loader.go b/d2core/d2records/pet_type_loader.go index 59d9f4a9..b8000f1d 100644 --- a/d2core/d2records/pet_type_loader.go +++ b/d2core/d2records/pet_type_loader.go @@ -39,7 +39,7 @@ func petTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d PetType records", len(records)) + r.Debugf("Loaded %d PetType records", len(records)) r.PetTypes = records diff --git a/d2core/d2records/player_class_loader.go b/d2core/d2records/player_class_loader.go index df6321ce..3eff68be 100644 --- a/d2core/d2records/player_class_loader.go +++ b/d2core/d2records/player_class_loader.go @@ -29,7 +29,7 @@ func playerClassLoader(r *RecordManager, d *d2txt.DataDictionary) error { return d.Err } - r.Logger.Infof("Loaded %d PlayerClass records", len(records)) + r.Debugf("Loaded %d PlayerClass records", len(records)) r.Character.Classes = records diff --git a/d2core/d2records/player_mode_loader.go b/d2core/d2records/player_mode_loader.go index 9274759f..010876c1 100644 --- a/d2core/d2records/player_mode_loader.go +++ b/d2core/d2records/player_mode_loader.go @@ -23,7 +23,7 @@ func playerModesLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Character.Modes = records - r.Logger.Infof("Loaded %d PlayerMode records", len(records)) + r.Debugf("Loaded %d PlayerMode records", len(records)) return nil } diff --git a/d2core/d2records/player_type_loader.go b/d2core/d2records/player_type_loader.go index e383df9a..60b7e2c3 100644 --- a/d2core/d2records/player_type_loader.go +++ b/d2core/d2records/player_type_loader.go @@ -24,7 +24,7 @@ func playerTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error { panic(d.Err) } - r.Logger.Infof("Loaded %d PlayerType records", len(records)) + r.Debugf("Loaded %d PlayerType records", len(records)) r.Animation.Token.Player = records diff --git a/d2core/d2records/property_loader.go b/d2core/d2records/property_loader.go index db718319..e68b1708 100644 --- a/d2core/d2records/property_loader.go +++ b/d2core/d2records/property_loader.go @@ -67,7 +67,7 @@ func propertyLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Properties = records - r.Logger.Infof("Loaded %d Property records", len(records)) + r.Debugf("Loaded %d Property records", len(records)) return nil } diff --git a/d2core/d2records/rare_prefix_loader.go b/d2core/d2records/rare_prefix_loader.go index 290eccf4..65512d0f 100644 --- a/d2core/d2records/rare_prefix_loader.go +++ b/d2core/d2records/rare_prefix_loader.go @@ -12,7 +12,7 @@ func rareItemPrefixLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.Rare.Prefix = records - r.Logger.Infof("Loaded %d RarePrefix records", len(records)) + r.Debugf("Loaded %d RarePrefix records", len(records)) return nil } diff --git a/d2core/d2records/rare_suffix_loader.go b/d2core/d2records/rare_suffix_loader.go index fb158cb7..94123695 100644 --- a/d2core/d2records/rare_suffix_loader.go +++ b/d2core/d2records/rare_suffix_loader.go @@ -10,7 +10,7 @@ func rareItemSuffixLoader(r *RecordManager, d *d2txt.DataDictionary) error { return err } - r.Logger.Infof("Loaded %d RareSuffix records", len(records)) + r.Debugf("Loaded %d RareSuffix records", len(records)) r.Item.Rare.Suffix = records diff --git a/d2core/d2records/record_manager.go b/d2core/d2records/record_manager.go index 716a4f4d..8afba41c 100644 --- a/d2core/d2records/record_manager.go +++ b/d2core/d2records/record_manager.go @@ -334,8 +334,8 @@ func (r *RecordManager) GetExperienceBreakpoint(heroType d2enum.Hero, level int) return r.Character.Experience[level].HeroBreakpoints[heroType] } -// GetLevelDetails gets a LevelDetailsRecord by the record Id -func (r *RecordManager) GetLevelDetails(id int) *LevelDetailsRecord { +// GetLevelDetails gets a LevelDetailRecord by the record Id +func (r *RecordManager) GetLevelDetails(id int) *LevelDetailRecord { for i := 0; i < len(r.Level.Details); i++ { if r.Level.Details[i].ID == id { return r.Level.Details[i] @@ -433,7 +433,7 @@ func (r *RecordManager) lookupObject(act, typ, id int) *ObjectLookupRecord { } // SelectSoundByIndex selects a sound by its ID -func (r *RecordManager) SelectSoundByIndex(index int) *SoundDetailsRecord { +func (r *RecordManager) SelectSoundByIndex(index int) *SoundDetailRecord { for idx := range r.Sound.Details { if r.Sound.Details[idx].Index == index { return r.Sound.Details[idx] diff --git a/d2core/d2records/runeword_loader.go b/d2core/d2records/runeword_loader.go index e498fe2d..a367d9d2 100644 --- a/d2core/d2records/runeword_loader.go +++ b/d2core/d2records/runeword_loader.go @@ -24,12 +24,12 @@ const ( fmtRunewordPropMax = "T1Max%d" ) -// Loadrecords loads runes records into a map[string]*RunesRecord +// Loadrecords loads runes records into a map[string]*RuneRecord func runewordLoader(r *RecordManager, d *d2txt.DataDictionary) error { - records := make(map[string]*RunesRecord) + records := make(map[string]*RuneRecord) for d.Next() { - record := &RunesRecord{ + record := &RuneRecord{ Name: d.String("name"), RuneName: d.String("Rune Name"), Complete: d.Bool("complete"), @@ -89,7 +89,7 @@ func runewordLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.Runewords = records - r.Logger.Infof("Loaded %d records records", len(records)) + r.Debugf("Loaded %d Rune records", len(records)) return nil } diff --git a/d2core/d2records/runeword_record.go b/d2core/d2records/runeword_record.go index cfd63461..7041a018 100644 --- a/d2core/d2records/runeword_record.go +++ b/d2core/d2records/runeword_record.go @@ -1,11 +1,11 @@ package d2records -// Runewords stores all of the RunesRecords -type Runewords map[string]*RunesRecord +// Runewords stores all of the RuneRecords +type Runewords map[string]*RuneRecord -// RunesRecord is a representation of a single row of runes.txt. It defines +// RuneRecord is a representation of a single row of runes.txt. It defines // runewords available in the game. -type RunesRecord struct { +type RuneRecord struct { Name string RuneName string // More of a note - the actual name should be read from the TBL files. Complete bool // An enabled/disabled flag. Only "Complete" runewords work in game. diff --git a/d2core/d2records/set_item_loader.go b/d2core/d2records/set_item_loader.go index e1dfff03..88021cbf 100644 --- a/d2core/d2records/set_item_loader.go +++ b/d2core/d2records/set_item_loader.go @@ -94,7 +94,7 @@ func setItemLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.SetItems = records - r.Logger.Infof("Loaded %d SetItem records", len(records)) + r.Debugf("Loaded %d SetItem records", len(records)) return nil } diff --git a/d2core/d2records/set_loader.go b/d2core/d2records/set_loader.go index 8babd544..5ac03637 100644 --- a/d2core/d2records/set_loader.go +++ b/d2core/d2records/set_loader.go @@ -107,7 +107,7 @@ func setLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.Sets = records - r.Logger.Infof("Loaded %d records records", len(records)) + r.Debugf("Loaded %d Set records", len(records)) return nil } diff --git a/d2core/d2records/shrine_loader.go b/d2core/d2records/shrine_loader.go index 7d143032..2aedf9bc 100644 --- a/d2core/d2records/shrine_loader.go +++ b/d2core/d2records/shrine_loader.go @@ -31,7 +31,7 @@ func shrineLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Object.Shrines = records - r.Logger.Infof("Loaded %d shrines", len(records)) + r.Debugf("Loaded %d Shrine records", len(records)) return nil } diff --git a/d2core/d2records/skill_description_loader.go b/d2core/d2records/skill_description_loader.go index 5eb1b96e..f579a8d5 100644 --- a/d2core/d2records/skill_description_loader.go +++ b/d2core/d2records/skill_description_loader.go @@ -139,7 +139,7 @@ func skillDescriptionLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Skill.Descriptions = records - r.Logger.Infof("Loaded %d Skill Description records", len(records)) + r.Debugf("Loaded %d SkillDescription records", len(records)) return nil } diff --git a/d2core/d2records/skill_details_loader.go b/d2core/d2records/skill_details_loader.go index da790eed..6067cc4c 100644 --- a/d2core/d2records/skill_details_loader.go +++ b/d2core/d2records/skill_details_loader.go @@ -277,7 +277,7 @@ func skillDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Skill.Details = records - r.Logger.Infof("Loaded %d Skill records", len(records)) + r.Debugf("Loaded %d Skill records", len(records)) return nil } diff --git a/d2core/d2records/sound_details_loader.go b/d2core/d2records/sound_details_loader.go index ff62077f..b3436f0d 100644 --- a/d2core/d2records/sound_details_loader.go +++ b/d2core/d2records/sound_details_loader.go @@ -9,7 +9,7 @@ func soundDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error { records := make(SoundDetails) for d.Next() { - entry := &SoundDetailsRecord{ + entry := &SoundDetailRecord{ Handle: d.String("Sound"), Index: d.Number("Index"), FileName: d.String("FileName"), @@ -46,7 +46,7 @@ func soundDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Sound.Details = records - r.Logger.Infof("Loaded %d sound definitions", len(records)) + r.Debugf("Loaded %d SoundDetail records", len(records)) return nil } diff --git a/d2core/d2records/sound_details_record.go b/d2core/d2records/sound_details_record.go index 56de22c5..4365998a 100644 --- a/d2core/d2records/sound_details_record.go +++ b/d2core/d2records/sound_details_record.go @@ -1,10 +1,10 @@ package d2records // SoundDetails is a map of the SoundEntries -type SoundDetails map[string]*SoundDetailsRecord +type SoundDetails map[string]*SoundDetailRecord -// SoundDetailsRecord represents a sound entry -type SoundDetailsRecord struct { +// SoundDetailRecord represents a sound entry +type SoundDetailRecord struct { Handle string FileName string Index int diff --git a/d2core/d2records/sound_environment_loader.go b/d2core/d2records/sound_environment_loader.go index 98c6bdea..68562ccc 100644 --- a/d2core/d2records/sound_environment_loader.go +++ b/d2core/d2records/sound_environment_loader.go @@ -44,7 +44,7 @@ func soundEnvironmentLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Sound.Environment = records - r.Logger.Infof("Loaded %d SoundEnviron records", len(records)) + r.Debugf("Loaded %d SoundEnviron records", len(records)) return nil } diff --git a/d2core/d2records/states_loader.go b/d2core/d2records/states_loader.go index 21798f82..b7724b3a 100644 --- a/d2core/d2records/states_loader.go +++ b/d2core/d2records/states_loader.go @@ -89,7 +89,7 @@ func statesLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.States = records - r.Logger.Infof("Loaded %d State records", len(records)) + r.Debugf("Loaded %d State records", len(records)) return nil } diff --git a/d2core/d2records/storepage_loader.go b/d2core/d2records/storepage_loader.go index 67655354..f66a422b 100644 --- a/d2core/d2records/storepage_loader.go +++ b/d2core/d2records/storepage_loader.go @@ -21,7 +21,7 @@ func storePagesLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.StorePages = records - r.Logger.Infof("Loaded %d StorePage records", len(records)) + r.Debugf("Loaded %d StorePage records", len(records)) return nil } diff --git a/d2core/d2records/treasure_class_loader.go b/d2core/d2records/treasure_class_loader.go index 8f858a96..e8fdbe7a 100644 --- a/d2core/d2records/treasure_class_loader.go +++ b/d2core/d2records/treasure_class_loader.go @@ -20,7 +20,7 @@ func treasureClassLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.Treasure.Normal = records - r.Logger.Infof("Loaded %d treasure class (normal) records", len(records)) + r.Debugf("Loaded %d TreasureClass (normal) records", len(records)) return nil } @@ -33,7 +33,7 @@ func treasureClassExLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.Treasure.Expansion = records - r.Logger.Infof("Loaded %d treasure class (expansion) records", len(records)) + r.Debugf("Loaded %d TreasureClass (expansion) records", len(records)) return nil } diff --git a/d2core/d2records/unique_appellation_loader.go b/d2core/d2records/unique_appellation_loader.go index e551f901..c6d3c614 100644 --- a/d2core/d2records/unique_appellation_loader.go +++ b/d2core/d2records/unique_appellation_loader.go @@ -21,7 +21,7 @@ func uniqueAppellationsLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Monster.Unique.Appellations = records - r.Logger.Infof("Loaded %d UniqueAppellation records", len(records)) + r.Debugf("Loaded %d UniqueAppellation records", len(records)) return nil } diff --git a/d2core/d2records/unique_items_loader.go b/d2core/d2records/unique_items_loader.go index a9ae79e6..639d2dbd 100644 --- a/d2core/d2records/unique_items_loader.go +++ b/d2core/d2records/unique_items_loader.go @@ -128,7 +128,7 @@ func uniqueItemsLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Item.Unique = records - r.Logger.Infof("Loaded %d unique items", len(records)) + r.Debugf("Loaded %d UniqueItem records", len(records)) return nil } diff --git a/d2core/d2records/weapon_class_loader.go b/d2core/d2records/weapon_class_loader.go index 76154b1e..84c0b569 100644 --- a/d2core/d2records/weapon_class_loader.go +++ b/d2core/d2records/weapon_class_loader.go @@ -22,7 +22,7 @@ func weaponClassesLoader(r *RecordManager, d *d2txt.DataDictionary) error { r.Animation.Token.Weapon = records - r.Logger.Infof("Loaded %d WeaponClass records", len(records)) + r.Debugf("Loaded %d WeaponClass records", len(records)) return nil } diff --git a/d2core/d2stats/diablo2stats/stat_test.go b/d2core/d2stats/diablo2stats/stat_test.go index cd54927c..e6ae66e2 100644 --- a/d2core/d2stats/diablo2stats/stat_test.go +++ b/d2core/d2stats/diablo2stats/stat_test.go @@ -238,12 +238,12 @@ 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}, } // 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", diff --git a/d2core/d2systems/file_source_resolver.go b/d2core/d2systems/file_source_resolver.go index f55b7fcc..cf860cfd 100644 --- a/d2core/d2systems/file_source_resolver.go +++ b/d2core/d2systems/file_source_resolver.go @@ -144,7 +144,7 @@ func (s *fsSource) Path() string { // mpq source func (m *FileSourceResolver) makeMpqSource(path string) (d2components.AbstractSource, error) { - mpq, err := d2mpq.Load(path) + mpq, err := d2mpq.FromFile(path) if err != nil { return nil, err } diff --git a/d2core/d2systems/file_type_resolver.go b/d2core/d2systems/file_type_resolver.go index 95ba97ad..7ff8cc27 100644 --- a/d2core/d2systems/file_type_resolver.go +++ b/d2core/d2systems/file_type_resolver.go @@ -88,7 +88,7 @@ func (m *FileTypeResolver) determineFileType(id akara.EID) { ft := m.Components.FileType.Add(id) // try to immediately load as an mpq - if _, err := d2mpq.Load(fp.Path); err == nil { + if _, err := d2mpq.New(fp.Path); err == nil { ft.Type = d2enum.FileTypeMPQ return } diff --git a/d2core/d2systems/render.go b/d2core/d2systems/render.go index bc44518c..964b1d9d 100644 --- a/d2core/d2systems/render.go +++ b/d2core/d2systems/render.go @@ -146,7 +146,6 @@ func (m *RenderSystem) createRenderer() { RunInBackground: config.RunInBackground, VsyncEnabled: config.VsyncEnabled, Backend: config.Backend, - LogLevel: config.LogLevel, } renderer, err := d2render.CreateRenderer(oldStyleConfig) diff --git a/d2core/d2systems/scene_base.go b/d2core/d2systems/scene_base.go index fb9258b8..f98c9bb5 100644 --- a/d2core/d2systems/scene_base.go +++ b/d2core/d2systems/scene_base.go @@ -5,6 +5,8 @@ import ( "image/color" "sort" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom/rectangle" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2scene" @@ -82,7 +84,7 @@ type BaseScene struct { } *d2util.Logger key string - booted bool + state d2enum.SceneState paused bool Add *sceneObjectFactory Viewports []akara.EID @@ -92,9 +94,14 @@ type BaseScene struct { gameConfigs *akara.Subscription } +// State returns the scene's current state +func (s *BaseScene) State() d2enum.SceneState { + return s.state +} + // Booted returns whether or not the scene has booted func (s *BaseScene) Booted() bool { - return s.booted + return s.state == d2enum.SceneStateBooted } // Paused returns whether or not the scene is paused @@ -113,17 +120,20 @@ func (s *BaseScene) Init(world *akara.World) { } func (s *BaseScene) boot() { - s.Debug("base scene booting ...") + if s.state == d2enum.SceneStateUninitialized { + s.Debug("base scene booting ...") - s.Add = &sceneObjectFactory{ - BaseScene: s, - Logger: d2util.NewLogger(), + s.Add = &sceneObjectFactory{ + BaseScene: s, + Logger: d2util.NewLogger(), + } + + s.Add.SetPrefix(fmt.Sprintf("%s -> %s", s.key, "Object Factory")) + + s.bindRequiredSystems() + s.state = d2enum.SceneStateBooting } - s.Add.SetPrefix(fmt.Sprintf("%s -> %s", s.key, "Object Factory")) - - s.bindRequiredSystems() - if !s.requiredSystemsPresent() { return } @@ -144,7 +154,7 @@ func (s *BaseScene) boot() { gameConfigs := s.NewComponentFilter().Require(&d2components.GameConfig{}).Build() s.gameConfigs = s.World.AddSubscription(gameConfigs) - s.booted = true + s.state = d2enum.SceneStateBooted } func (s *BaseScene) bindRequiredSystems() { @@ -205,23 +215,23 @@ func (s *BaseScene) setupSceneObjectFactories() { func (s *BaseScene) setupFactories() { s.Debug("setting up component factories") - s.InjectComponent(&d2components.MainViewport{}, &s.Components.MainViewport.ComponentFactory) + s.InjectComponent(&d2components.SceneGraphNode{}, &s.Components.SceneGraphNode.ComponentFactory) s.InjectComponent(&d2components.Viewport{}, &s.Components.Viewport.ComponentFactory) + s.InjectComponent(&d2components.MainViewport{}, &s.Components.MainViewport.ComponentFactory) s.InjectComponent(&d2components.ViewportFilter{}, &s.Components.ViewportFilter.ComponentFactory) - s.InjectComponent(&d2components.Camera{}, &s.Components.Camera.ComponentFactory) s.InjectComponent(&d2components.Priority{}, &s.Components.Priority.ComponentFactory) + s.InjectComponent(&d2components.Camera{}, &s.Components.Camera.ComponentFactory) s.InjectComponent(&d2components.Texture{}, &s.Components.Texture.ComponentFactory) s.InjectComponent(&d2components.Interactive{}, &s.Components.Interactive.ComponentFactory) s.InjectComponent(&d2components.Transform{}, &s.Components.Transform.ComponentFactory) - s.InjectComponent(&d2components.Origin{}, &s.Components.Origin.ComponentFactory) - s.InjectComponent(&d2components.Alpha{}, &s.Components.Alpha.ComponentFactory) - s.InjectComponent(&d2components.SceneGraphNode{}, &s.Components.SceneGraphNode.ComponentFactory) - s.InjectComponent(&d2components.DrawEffect{}, &s.Components.DrawEffect.ComponentFactory) s.InjectComponent(&d2components.Sprite{}, &s.Components.Sprite.ComponentFactory) s.InjectComponent(&d2components.SegmentedSprite{}, &s.Components.SegmentedSprite.ComponentFactory) + s.InjectComponent(&d2components.Origin{}, &s.Components.Origin.ComponentFactory) + s.InjectComponent(&d2components.Alpha{}, &s.Components.Alpha.ComponentFactory) + s.InjectComponent(&d2components.DrawEffect{}, &s.Components.DrawEffect.ComponentFactory) s.InjectComponent(&d2components.Rectangle{}, &s.Components.Rectangle.ComponentFactory) - s.InjectComponent(&d2components.Checkbox{}, &s.Components.Checkbox.ComponentFactory) s.InjectComponent(&d2components.Label{}, &s.Components.Label.ComponentFactory) + s.InjectComponent(&d2components.Checkbox{}, &s.Components.Checkbox.ComponentFactory) s.InjectComponent(&d2components.Color{}, &s.Components.Color.ComponentFactory) s.InjectComponent(&d2components.CommandRegistration{}, &s.Components.CommandRegistration.ComponentFactory) s.InjectComponent(&d2components.Dirty{}, &s.Components.Dirty.ComponentFactory) @@ -235,11 +245,8 @@ func (s *BaseScene) Key() string { // Update performs scene boot and renders the scene viewports func (s *BaseScene) Update() { - if !s.booted { + if !s.Booted() { s.boot() - } - - if !s.booted { return } @@ -453,12 +460,13 @@ func (s *BaseScene) renderViewportsToMainViewport() { } // RegisterTerminalCommand registers a command that can be executed from the terminal -func (s *BaseScene) RegisterTerminalCommand(name, desc string, fn interface{}) { +func (s *BaseScene) RegisterTerminalCommand(name, desc string, args []string, fn func(args []string) error) { regID := s.NewEntity() reg := s.Components.CommandRegistration.Add(regID) s.Components.Dirty.Add(regID) reg.Name = name reg.Description = desc + reg.Args = args reg.Callback = fn } diff --git a/d2core/d2systems/scene_ebiten_splash.go b/d2core/d2systems/scene_ebiten_splash.go index ca6d742d..09756cc3 100644 --- a/d2core/d2systems/scene_ebiten_splash.go +++ b/d2core/d2systems/scene_ebiten_splash.go @@ -4,6 +4,8 @@ import ( "image/color" "math" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/gravestench/akara" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2input" @@ -39,7 +41,7 @@ func NewEbitenSplashScene() *EbitenSplashScene { // EbitenSplashScene represents the in-game terminal for typing commands type EbitenSplashScene struct { *BaseScene - booted bool + state d2enum.SceneState squares []akara.EID timeElapsed float64 delay float64 @@ -53,14 +55,14 @@ func (s *EbitenSplashScene) Init(world *akara.World) { } func (s *EbitenSplashScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } s.createSplash() - s.booted = true + s.state = d2enum.SceneStateBooted } // Update and render the terminal to the terminal viewport @@ -73,11 +75,11 @@ func (s *EbitenSplashScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } - if !s.booted { + if s.state != d2enum.SceneStateBooted { return } diff --git a/d2core/d2systems/scene_loading_screen.go b/d2core/d2systems/scene_loading_screen.go index 463402cb..0fdd031e 100644 --- a/d2core/d2systems/scene_loading_screen.go +++ b/d2core/d2systems/scene_loading_screen.go @@ -4,6 +4,8 @@ import ( "image/color" "math" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/gravestench/akara" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -37,9 +39,10 @@ type LoadingScene struct { stage2 *akara.Subscription // has type, no handle stage3 *akara.Subscription // has handle, no asset stage4 *akara.Subscription // is loaded + total *akara.Subscription // total files } progress float64 - booted bool + state d2enum.SceneState } // Init the loading scene @@ -56,6 +59,15 @@ func (s *LoadingScene) Init(world *akara.World) { func (s *LoadingScene) setupSubscriptions() { s.Debug("setting up component subscriptions") + total := s.NewComponentFilter(). + Require( + &d2components.File{}, + ). + Forbid( + &d2components.FileSource{}, + ). + Build() + stage1 := s.NewComponentFilter(). Require( &d2components.File{}, @@ -102,6 +114,7 @@ func (s *LoadingScene) setupSubscriptions() { ). Build() + s.loadStages.total = s.World.AddSubscription(total) // total count of all files at all stages s.loadStages.stage1 = s.World.AddSubscription(stage1) // has path, no type s.loadStages.stage2 = s.World.AddSubscription(stage2) // has type, no handle s.loadStages.stage3 = s.World.AddSubscription(stage3) // has handle, no asset @@ -109,14 +122,14 @@ func (s *LoadingScene) setupSubscriptions() { } func (s *LoadingScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } s.createLoadingScreen() - s.booted = true + s.state = d2enum.SceneStateBooted } func (s *LoadingScene) createLoadingScreen() { @@ -134,10 +147,14 @@ func (s *LoadingScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } + if s.state != d2enum.SceneStateBooted { + return + } + s.updateLoadProgress() s.updateViewportAlpha() s.updateLoadingSpritePosition() @@ -147,12 +164,10 @@ func (s *LoadingScene) Update() { } func (s *LoadingScene) updateLoadProgress() { - untyped := float64(len(s.loadStages.stage1.GetEntities())) - unhandled := float64(len(s.loadStages.stage2.GetEntities())) - unparsed := float64(len(s.loadStages.stage3.GetEntities())) + total := float64(len(s.loadStages.total.GetEntities())) loaded := float64(len(s.loadStages.stage4.GetEntities())) - s.progress = 1 - ((untyped + unhandled + unparsed) / 3 / loaded) + s.progress = loaded / total } //nolint:gomnd // arbitrary numbers for test scene diff --git a/d2core/d2systems/scene_main_menu.go b/d2core/d2systems/scene_main_menu.go index a8a31e7b..26924b7b 100644 --- a/d2core/d2systems/scene_main_menu.go +++ b/d2core/d2systems/scene_main_menu.go @@ -39,8 +39,8 @@ var _ d2interface.Scene = &MainMenuScene{} // or start the map engine test. type MainMenuScene struct { *BaseScene - booted bool logoInit bool + state d2enum.SceneState sprites struct { trademark akara.EID logoFireLeft akara.EID @@ -59,18 +59,20 @@ func (s *MainMenuScene) Init(world *akara.World) { } func (s *MainMenuScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } + s.state = d2enum.SceneStateBooting + s.setupViewports() s.createBackground() s.createButtons() s.createTrademarkScreen() s.createLogo() - s.booted = true + s.state = d2enum.SceneStateBooted } func (s *MainMenuScene) setupViewports() { @@ -170,10 +172,14 @@ func (s *MainMenuScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } + if s.state != d2enum.SceneStateBooted { + return + } + if !s.logoInit { s.Debug("attempting logo sprite init") s.initLogoSprites() diff --git a/d2core/d2systems/scene_mouse_cursor.go b/d2core/d2systems/scene_mouse_cursor.go index 987e1119..a36c891a 100644 --- a/d2core/d2systems/scene_mouse_cursor.go +++ b/d2core/d2systems/scene_mouse_cursor.go @@ -1,9 +1,13 @@ package d2systems import ( + "fmt" "math" + "strconv" "time" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/gravestench/akara" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -35,7 +39,7 @@ type MouseCursorScene struct { lastTimeMoved time.Time *BaseScene cursor akara.EID - booted bool + state d2enum.SceneState debug struct { enabled bool } @@ -49,7 +53,7 @@ func (s *MouseCursorScene) Init(world *akara.World) { } func (s *MouseCursorScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } @@ -58,7 +62,7 @@ func (s *MouseCursorScene) boot() { s.createMouseCursor() - s.booted = true + s.state = d2enum.SceneStateBooted } func (s *MouseCursorScene) createMouseCursor() { @@ -76,10 +80,14 @@ func (s *MouseCursorScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } + if s.state != d2enum.SceneStateBooted { + return + } + s.updateCursorTransform() s.handleCursorFade() @@ -141,8 +149,15 @@ func (s *MouseCursorScene) registerDebugCommand() { description = "show debug information about the mouse" ) - s.RegisterTerminalCommand(command, description, func(val bool) { + s.RegisterTerminalCommand(command, description, []string{"val"}, func(args []string) error { + val, err := strconv.ParseBool(args[0]) + if err != nil { + return fmt.Errorf("invalid argument") + } + s.setDebug(val) + + return nil }) } diff --git a/d2core/d2systems/scene_terminal.go b/d2core/d2systems/scene_terminal.go index 961a0c11..ad995007 100644 --- a/d2core/d2systems/scene_terminal.go +++ b/d2core/d2systems/scene_terminal.go @@ -4,6 +4,8 @@ import ( "image/color" "time" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2components" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" @@ -44,7 +46,7 @@ type TerminalScene struct { d2interface.Terminal d2interface.InputManager commandsToRegister *akara.Subscription - booted bool + state d2enum.SceneState } // Init the terminal @@ -70,14 +72,14 @@ func (s *TerminalScene) setupSubscriptions() { } func (s *TerminalScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } s.createTerminal() - s.booted = true + s.state = d2enum.SceneStateBooted } // Update and render the terminal to the terminal viewport @@ -90,11 +92,11 @@ func (s *TerminalScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } - if !s.booted { + if s.state != d2enum.SceneStateBooted { return } @@ -118,7 +120,7 @@ func (s *TerminalScene) processCommand(eid akara.EID) { s.Infof("Registering command `%s` - %s", reg.Name, reg.Description) - err := s.Terminal.BindAction(reg.Name, reg.Description, reg.Callback) + err := s.Terminal.Bind(reg.Name, reg.Description, nil, reg.Callback) if err != nil { s.Error(err.Error()) } diff --git a/d2core/d2systems/scene_test_button.go b/d2core/d2systems/scene_test_button.go index 99320a4e..0fa36b5f 100644 --- a/d2core/d2systems/scene_test_button.go +++ b/d2core/d2systems/scene_test_button.go @@ -3,6 +3,8 @@ package d2systems import ( "github.com/gravestench/akara" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2button" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -30,7 +32,7 @@ var _ d2interface.Scene = &ButtonTestScene{} // or start the map engine test. type ButtonTestScene struct { *BaseScene - booted bool + state d2enum.SceneState buttons *akara.Subscription } @@ -49,7 +51,7 @@ func (s *ButtonTestScene) Init(world *akara.World) { } func (s *ButtonTestScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } @@ -58,7 +60,7 @@ func (s *ButtonTestScene) boot() { s.createButtons() - s.booted = true + s.state = d2enum.SceneStateBooted } func (s *ButtonTestScene) createButtons() { @@ -71,10 +73,14 @@ func (s *ButtonTestScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } + if s.state != d2enum.SceneStateBooted { + return + } + for _, eid := range s.buttons.GetEntities() { s.updateButtonPosition(eid) } diff --git a/d2core/d2systems/scene_test_checkbox.go b/d2core/d2systems/scene_test_checkbox.go index bee3b626..6899b8fd 100644 --- a/d2core/d2systems/scene_test_checkbox.go +++ b/d2core/d2systems/scene_test_checkbox.go @@ -4,6 +4,8 @@ import ( "image/color" "log" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/gravestench/akara" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -31,7 +33,7 @@ var _ d2interface.Scene = &CheckboxTestScene{} // or start the map engine test. type CheckboxTestScene struct { *BaseScene - booted bool + state d2enum.SceneState checkboxes *akara.Subscription } @@ -50,7 +52,7 @@ func (s *CheckboxTestScene) Init(world *akara.World) { } func (s *CheckboxTestScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } @@ -66,7 +68,7 @@ func (s *CheckboxTestScene) boot() { s.createCheckboxes() - s.booted = true + s.state = d2enum.SceneStateBooted } //nolint:gomnd // arbitrary example numbers for test @@ -84,10 +86,14 @@ func (s *CheckboxTestScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } + if s.state != d2enum.SceneStateBooted { + return + } + s.BaseScene.Update() } diff --git a/d2core/d2systems/scene_test_label.go b/d2core/d2systems/scene_test_label.go index 735d6413..2d546b93 100644 --- a/d2core/d2systems/scene_test_label.go +++ b/d2core/d2systems/scene_test_label.go @@ -4,6 +4,8 @@ import ( "image/color" "math/rand" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/gravestench/akara" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -32,7 +34,7 @@ var _ d2interface.Scene = &LabelTestScene{} // or start the map engine test. type LabelTestScene struct { *BaseScene - booted bool + state d2enum.SceneState labels *akara.Subscription velocity d2components.VelocityFactory } @@ -50,7 +52,7 @@ func (s *LabelTestScene) Init(world *akara.World) { } func (s *LabelTestScene) boot() { - if !s.BaseScene.booted { + if !s.BaseScene.Booted() { s.BaseScene.boot() return } @@ -59,7 +61,7 @@ func (s *LabelTestScene) boot() { s.createLabels() - s.booted = true + s.state = d2enum.SceneStateBooted } //nolint:gosec,gomnd // test scene, weak RNG is fine @@ -111,9 +113,13 @@ func (s *LabelTestScene) Update() { return } - if !s.booted { + if s.state == d2enum.SceneStateUninitialized { s.boot() } + if s.state != d2enum.SceneStateBooted { + return + } + s.BaseScene.Update() } diff --git a/d2core/d2systems/scene_widget_system.go b/d2core/d2systems/scene_widget_system.go index e9779557..c6ff5d23 100644 --- a/d2core/d2systems/scene_widget_system.go +++ b/d2core/d2systems/scene_widget_system.go @@ -5,6 +5,8 @@ import ( "image/color" "time" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/gravestench/akara" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache" @@ -75,11 +77,11 @@ type UIWidgetFactory struct { buttonLoadQueue checkboxLoadQueue labelLoadQueue + state d2enum.SceneState bitmapFontCache d2interface.Cache labelsToUpdate *akara.Subscription buttonsToUpdate *akara.Subscription checkboxesToUpdate *akara.Subscription - booted bool Components struct { File d2components.FileFactory Transform d2components.TransformFactory @@ -155,7 +157,7 @@ func (t *UIWidgetFactory) boot() { return } - t.booted = true + t.state = d2enum.SceneStateBooted } // Update processes the load queues and update the widgets. The load queues are necessary because @@ -163,7 +165,7 @@ func (t *UIWidgetFactory) boot() { func (t *UIWidgetFactory) Update() { start := time.Now() - if !t.booted { + if t.state != d2enum.SceneStateBooted { t.boot() return } diff --git a/d2core/d2systems/timescale.go b/d2core/d2systems/timescale.go index a61b13c9..af878bce 100644 --- a/d2core/d2systems/timescale.go +++ b/d2core/d2systems/timescale.go @@ -1,6 +1,8 @@ package d2systems import ( + "fmt" + "strconv" "time" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2components" @@ -62,15 +64,21 @@ func (t *TimeScaleSystem) Update() { func (t *TimeScaleSystem) registerCommands() { e := t.NewEntity() - reg := t.Components.CommandRegistration.Add(e) - t.Components.Dirty.Add(e) reg.Name = "timescale" reg.Description = "set the time scale of the game (default is 1.0)" - reg.Callback = func(scale float64) { - t.Infof("setting time scale to %.1f", scale) - t.scale = scale + reg.Args = []string{"scale"} + reg.Callback = func(args []string) error { + val, err := strconv.ParseFloat(args[0], 64) + if err != nil { + return fmt.Errorf("invalid argument") + } + + t.Infof("setting time scale to %.1f", val) + t.scale = val + + return nil } } diff --git a/d2core/d2term/commmand.go b/d2core/d2term/commmand.go new file mode 100644 index 00000000..d72a121d --- /dev/null +++ b/d2core/d2term/commmand.go @@ -0,0 +1,33 @@ +package d2term + +import ( + "sort" +) + +func (t *Terminal) commandList([]string) error { + names := make([]string, 0, len(t.commands)) + for name := range t.commands { + names = append(names, name) + } + + sort.Strings(names) + t.Infof("available actions (%d):", len(names)) + + for _, name := range names { + entry := t.commands[name] + if entry.arguments != nil { + t.Infof("%s: %s; %v", name, entry.description, entry.arguments) + continue + } + + t.Infof("%s: %s", name, entry.description) + } + + return nil +} + +func (t *Terminal) commandClear([]string) error { + t.Clear() + + return nil +} diff --git a/d2core/d2term/d2term.go b/d2core/d2term/d2term.go index 561b144b..4e478784 100644 --- a/d2core/d2term/d2term.go +++ b/d2core/d2term/d2term.go @@ -6,8 +6,8 @@ import ( ) // New creates and initializes the terminal -func New(inputManager d2interface.InputManager) (d2interface.Terminal, error) { - term, err := createTerminal() +func New(inputManager d2interface.InputManager) (*Terminal, error) { + term, err := NewTerminal() if err != nil { return nil, err } diff --git a/d2core/d2term/terminal.go b/d2core/d2term/terminal.go index f83a5089..465905a0 100644 --- a/d2core/d2term/terminal.go +++ b/d2core/d2term/terminal.go @@ -6,24 +6,22 @@ import ( "image/color" "log" "math" - "reflect" - "sort" - "strconv" "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" ) const ( - termCharWidth = 6 - termCharHeight = 16 - termCharDoubleWidth = termCharWidth * 2 - termRowCount = 24 - termRowCountMax = 32 - termColCountMax = 128 - termAnimLength = 0.5 + charWidth = 6 + charHeight = 16 + charDoubleWidth = charWidth * 2 + rowCount = 24 + rowCountMax = 32 + colCountMax = 128 + animLength = 0.5 ) const ( @@ -34,13 +32,13 @@ const ( red = 0xcc0000b0 ) -type termVis int +type visibility int const ( - termVisHidden termVis = iota - termVisShowing - termVisShown - termVisHiding + visHidden visibility = iota + visShowing + visShown + visHiding ) const ( @@ -48,18 +46,20 @@ const ( minVisAnim = 0.0 ) -type termHistoryEntry struct { +type historyEntry struct { text string category d2enum.TermCategory } -type termActionEntry struct { - action interface{} +type commandEntry struct { description string + arguments []string + fn func([]string) error } -type terminal struct { - outputHistory []termHistoryEntry +// Terminal handles the in-game terminal +type Terminal struct { + outputHistory []historyEntry outputIndex int command string @@ -67,7 +67,7 @@ type terminal struct { commandIndex int lineCount int - visState termVis + visState visibility visAnim float64 bgColor color.RGBA @@ -76,36 +76,88 @@ type terminal struct { warningColor color.RGBA errorColor color.RGBA - actions map[string]termActionEntry + commands map[string]commandEntry } -func (t *terminal) Advance(elapsed float64) error { +// NewTerminal creates and returns a terminal +func NewTerminal() (*Terminal, error) { + term := &Terminal{ + lineCount: rowCount, + bgColor: d2util.Color(darkGrey), + fgColor: d2util.Color(lightGrey), + infoColor: d2util.Color(lightBlue), + warningColor: d2util.Color(yellow), + errorColor: d2util.Color(red), + commands: make(map[string]commandEntry), + } + + term.Infof("::: OpenDiablo2 Terminal :::") + term.Infof("type \"ls\" for a list of commands") + + if err := term.Bind("ls", "list available commands", nil, term.commandList); err != nil { + return nil, err + } + + if err := term.Bind("clear", "clear terminal", nil, term.commandClear); err != nil { + return nil, err + } + + return term, nil +} + +// Bind binds commands to the terminal +func (t *Terminal) Bind(name, description string, arguments []string, fn func(args []string) error) error { + if name == "" || description == "" { + return fmt.Errorf("missing name or description") + } + + if _, ok := t.commands[name]; ok { + t.Warningf("rebinding command with name: %s", name) + } + + t.commands[name] = commandEntry{description, arguments, fn} + + return nil +} + +// Unbind unbinds commands from the terminal +func (t *Terminal) Unbind(names ...string) error { + for _, name := range names { + delete(t.commands, name) + } + + return nil +} + +// Advance advances the terminal animation +func (t *Terminal) Advance(elapsed float64) error { switch t.visState { - case termVisShowing: - t.visAnim = math.Min(maxVisAnim, t.visAnim+elapsed/termAnimLength) + case visShowing: + t.visAnim = math.Min(maxVisAnim, t.visAnim+elapsed/animLength) if t.visAnim == maxVisAnim { - t.visState = termVisShown + t.visState = visShown } - case termVisHiding: - t.visAnim = math.Max(minVisAnim, t.visAnim-elapsed/termAnimLength) + case visHiding: + t.visAnim = math.Max(minVisAnim, t.visAnim-elapsed/animLength) if t.visAnim == minVisAnim { - t.visState = termVisHidden + t.visState = visHidden } } - if !t.IsVisible() { + if !t.Visible() { return nil } return nil } -func (t *terminal) OnKeyDown(event d2interface.KeyEvent) bool { +// OnKeyDown handles key down in the terminal +func (t *Terminal) OnKeyDown(event d2interface.KeyEvent) bool { if event.Key() == d2enum.KeyGraveAccent { - t.toggleTerminal() + t.toggle() } - if !t.IsVisible() { + if !t.Visible() { return false } @@ -138,7 +190,7 @@ func (t *terminal) OnKeyDown(event d2interface.KeyEvent) bool { return true } -func (t *terminal) processCommand() { +func (t *Terminal) processCommand() { if t.command == "" { return } @@ -155,17 +207,17 @@ func (t *terminal) processCommand() { t.commandHistory = t.commandHistory[:n] t.commandHistory = append(t.commandHistory, t.command) - t.Outputf(t.command) + t.Printf(t.command) if err := t.Execute(t.command); err != nil { - t.OutputErrorf(err.Error()) + t.Errorf(err.Error()) } t.commandIndex = len(t.commandHistory) - 1 t.command = "" } -func (t *terminal) handleControlKey(eventKey d2enum.Key, keyMod d2enum.KeyMod) { +func (t *Terminal) handleControlKey(eventKey d2enum.Key, keyMod d2enum.KeyMod) { switch eventKey { case d2enum.KeyUp: if keyMod == d2enum.KeyModControl { @@ -180,21 +232,14 @@ func (t *terminal) handleControlKey(eventKey d2enum.Key, keyMod d2enum.KeyMod) { } case d2enum.KeyDown: if keyMod == d2enum.KeyModControl { - t.lineCount = d2math.MinInt(t.lineCount+1, termRowCountMax) + t.lineCount = d2math.MinInt(t.lineCount+1, rowCountMax) } } } -func (t *terminal) toggleTerminal() { - if t.visState == termVisHiding || t.visState == termVisHidden { - t.Show() - } else { - t.Hide() - } -} - -func (t *terminal) OnKeyChars(event d2interface.KeyCharsEvent) bool { - if !t.IsVisible() { +// OnKeyChars handles char key in terminal +func (t *Terminal) OnKeyChars(event d2interface.KeyCharsEvent) bool { + if !t.Visible() { return false } @@ -210,14 +255,15 @@ func (t *terminal) OnKeyChars(event d2interface.KeyCharsEvent) bool { return handled } -func (t *terminal) Render(surface d2interface.Surface) error { - if !t.IsVisible() { +// Render renders the terminal +func (t *Terminal) Render(surface d2interface.Surface) error { + if !t.Visible() { return nil } totalWidth, _ := surface.GetSize() - outputHeight := t.lineCount * termCharHeight - totalHeight := outputHeight + termCharHeight + outputHeight := t.lineCount * charHeight + totalHeight := outputHeight + charHeight offset := -int((1.0 - easeInOut(t.visAnim)) * float64(totalHeight)) surface.PushTranslation(0, offset) @@ -230,19 +276,19 @@ func (t *terminal) Render(surface d2interface.Surface) error { break } - historyEntry := t.outputHistory[historyIndex] + entry := t.outputHistory[historyIndex] - surface.PushTranslation(termCharDoubleWidth, outputHeight-(i+1)*termCharHeight) - surface.DrawTextf(historyEntry.text) - surface.PushTranslation(-termCharDoubleWidth, 0) + surface.PushTranslation(charDoubleWidth, outputHeight-(i+1)*charHeight) + surface.DrawTextf(entry.text) + surface.PushTranslation(-charDoubleWidth, 0) - switch historyEntry.category { + switch entry.category { case d2enum.TermCategoryInfo: - surface.DrawRect(termCharWidth, termCharHeight, t.infoColor) + surface.DrawRect(charWidth, charHeight, t.infoColor) case d2enum.TermCategoryWarning: - surface.DrawRect(termCharWidth, termCharHeight, t.warningColor) + surface.DrawRect(charWidth, charHeight, t.warningColor) case d2enum.TermCategoryError: - surface.DrawRect(termCharWidth, termCharHeight, t.errorColor) + surface.DrawRect(charWidth, charHeight, t.errorColor) } surface.Pop() @@ -250,7 +296,7 @@ func (t *terminal) Render(surface d2interface.Surface) error { } surface.PushTranslation(0, outputHeight) - surface.DrawRect(totalWidth, termCharHeight, t.fgColor) + surface.DrawRect(totalWidth, charHeight, t.fgColor) surface.DrawTextf("> " + t.command) surface.Pop() @@ -259,184 +305,105 @@ func (t *terminal) Render(surface d2interface.Surface) error { return nil } -func (t *terminal) Execute(command string) error { +// Execute executes a command with arguments +func (t *Terminal) Execute(command string) error { params := parseCommand(command) if len(params) == 0 { return errors.New("invalid command") } - actionName := params[0] - actionParams := params[1:] + name := params[0] + args := params[1:] - actionEntry, ok := t.actions[actionName] + entry, ok := t.commands[name] if !ok { - return errors.New("action not found") + return errors.New("command not found") } - actionType := reflect.TypeOf(actionEntry.action) - if actionType.Kind() != reflect.Func { - return errors.New("action is not a function") + if len(args) != len(entry.arguments) { + return errors.New("command requires different argument count") } - if len(actionParams) != actionType.NumIn() { - return errors.New("action requires different argument count") - } - - paramValues, err := parseActionParams(actionType, actionParams) - if err != nil { + if err := entry.fn(args); err != nil { return err } - actionValue := reflect.ValueOf(actionEntry.action) - actionReturnValues := actionValue.Call(paramValues) - - if actionReturnValueCount := len(actionReturnValues); actionReturnValueCount > 0 { - t.OutputInfof("function returned %d values:", actionReturnValueCount) - - for _, actionReturnValue := range actionReturnValues { - t.OutputInfof("%v: %s", actionReturnValue.Interface(), actionReturnValue.String()) - } - } - return nil } -func parseActionParams(actionType reflect.Type, actionParams []string) ([]reflect.Value, error) { - var paramValues []reflect.Value +// Rawf writes a raw message to the terminal +func (t *Terminal) Rawf(category d2enum.TermCategory, format string, params ...interface{}) { + text := fmt.Sprintf(format, params...) + lines := d2util.SplitIntoLinesWithMaxWidth(text, colCountMax) - for i := 0; i < actionType.NumIn(); i++ { - actionParam := actionParams[i] + for _, line := range lines { + // removes color token (this token ends with [0m ) + l := strings.Split(line, "\033[0m") + line = l[len(l)-1] - switch actionType.In(i).Kind() { - case reflect.String: - paramValues = append(paramValues, reflect.ValueOf(actionParam)) - case reflect.Int: - value, err := strconv.ParseInt(actionParam, 10, 64) - if err != nil { - return nil, err - } - - paramValues = append(paramValues, reflect.ValueOf(int(value))) - case reflect.Uint: - value, err := strconv.ParseUint(actionParam, 10, 64) - if err != nil { - return nil, err - } - - paramValues = append(paramValues, reflect.ValueOf(uint(value))) - case reflect.Float64: - value, err := strconv.ParseFloat(actionParam, 64) - if err != nil { - return nil, err - } - - paramValues = append(paramValues, reflect.ValueOf(value)) - case reflect.Bool: - value, err := strconv.ParseBool(actionParam) - if err != nil { - return nil, err - } - - paramValues = append(paramValues, reflect.ValueOf(value)) - default: - return nil, errors.New("action has unsupported arguments") - } + t.outputHistory = append(t.outputHistory, historyEntry{line, category}) } - - return paramValues, nil } -func (t *terminal) OutputRaw(text string, category d2enum.TermCategory) { - var line string - - for _, word := range strings.Split(text, " ") { - if len(line) > 0 { - line += " " - } - - lineLength := len(line) - wordLength := len(word) - - if lineLength+wordLength >= termColCountMax { - t.outputHistory = append(t.outputHistory, termHistoryEntry{line, category}) - line = word - } else { - line += word - } - } - - t.outputHistory = append(t.outputHistory, termHistoryEntry{line, category}) +// Printf writes a message to the terminal +func (t *Terminal) Printf(format string, params ...interface{}) { + t.Rawf(d2enum.TermCategoryNone, format, params...) } -func (t *terminal) Outputf(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryNone) +// Infof writes a warning message to the terminal +func (t *Terminal) Infof(format string, params ...interface{}) { + t.Rawf(d2enum.TermCategoryInfo, format, params...) } -func (t *terminal) OutputInfof(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryInfo) +// Warningf writes a warning message to the terminal +func (t *Terminal) Warningf(format string, params ...interface{}) { + t.Rawf(d2enum.TermCategoryWarning, format, params...) } -func (t *terminal) OutputWarningf(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryWarning) +// Errorf writes a error message to the terminal +func (t *Terminal) Errorf(format string, params ...interface{}) { + t.Rawf(d2enum.TermCategoryError, format, params...) } -func (t *terminal) OutputErrorf(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryError) -} - -func (t *terminal) OutputClear() { +// Clear clears the terminal +func (t *Terminal) Clear() { t.outputHistory = nil t.outputIndex = 0 } -func (t *terminal) IsVisible() bool { - return t.visState != termVisHidden +// Visible returns visible state +func (t *Terminal) Visible() bool { + return t.visState != visHidden } -func (t *terminal) Hide() { - if t.visState != termVisHidden { - t.visState = termVisHiding +// Hide hides the terminal +func (t *Terminal) Hide() { + if t.visState != visHidden { + t.visState = visHiding } } -func (t *terminal) Show() { - if t.visState != termVisShown { - t.visState = termVisShowing +// Show shows the terminal +func (t *Terminal) Show() { + if t.visState != visShown { + t.visState = visShowing } } -func (t *terminal) BindAction(name, description string, action interface{}) error { - actionType := reflect.TypeOf(action) - if actionType.Kind() != reflect.Func { - return errors.New("action is not a function") +func (t *Terminal) toggle() { + if t.visState == visHiding || t.visState == visHidden { + t.Show() + return } - for i := 0; i < actionType.NumIn(); i++ { - switch actionType.In(i).Kind() { - case reflect.String: - case reflect.Int: - case reflect.Uint: - case reflect.Float64: - case reflect.Bool: - default: - return errors.New("action has unsupported arguments") - } - } - - t.actions[name] = termActionEntry{action, description} - - return nil + t.Hide() } -func (t *terminal) BindLogger() { +// BindLogger binds a log.Writer to the output +func (t *Terminal) BindLogger() { log.SetOutput(&terminalLogger{writer: log.Writer(), terminal: t}) } -func (t *terminal) UnbindAction(name string) error { - delete(t.actions, name) - return nil -} - func easeInOut(t float64) float64 { t *= 2 if t < 1 { @@ -490,70 +457,3 @@ func parseCommand(command string) []string { return params } - -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 -} - -func createTerminal() (*terminal, error) { - terminal := &terminal{ - lineCount: termRowCount, - bgColor: rgbaColor(darkGrey), - fgColor: rgbaColor(lightGrey), - infoColor: rgbaColor(lightBlue), - warningColor: rgbaColor(yellow), - errorColor: rgbaColor(red), - actions: make(map[string]termActionEntry), - } - - terminal.OutputInfof("::: OpenDiablo2 Terminal :::") - terminal.OutputInfof("type \"ls\" for a list of actions") - - err := terminal.BindAction("ls", "list available actions", func() { - var names []string - for name := range terminal.actions { - names = append(names, name) - } - - sort.Strings(names) - - terminal.OutputInfof("available actions (%d):", len(names)) - for _, name := range names { - entry := terminal.actions[name] - terminal.OutputInfof("%s: %s; %s", name, entry.description, reflect.TypeOf(entry.action).String()) - } - }) - if err != nil { - return nil, fmt.Errorf("failed to bind the '%s' action, err: %w", "ls", err) - } - - err = terminal.BindAction("clear", "clear terminal", func() { - terminal.OutputClear() - }) - if err != nil { - return nil, fmt.Errorf("failed to bind the '%s' action, err: %w", "clear", err) - } - - return terminal, nil -} diff --git a/d2core/d2term/terminal_logger.go b/d2core/d2term/terminal_logger.go index 62eb67c8..5e0dbb08 100644 --- a/d2core/d2term/terminal_logger.go +++ b/d2core/d2term/terminal_logger.go @@ -8,7 +8,7 @@ import ( ) type terminalLogger struct { - terminal *terminal + terminal *Terminal buffer bytes.Buffer writer io.Writer } @@ -31,16 +31,16 @@ func (tl *terminalLogger) Write(p []byte) (int, error) { switch { case strings.Index(lineLower, "error") > 0: - tl.terminal.OutputErrorf(line) + tl.terminal.Errorf(line) case strings.Index(lineLower, "warning") > 0: - tl.terminal.OutputWarningf(line) + tl.terminal.Warningf(line) default: - tl.terminal.Outputf(line) + tl.terminal.Printf(line) } return tl.writer.Write(p) } -func (tl *terminalLogger) BindToTerminal(t *terminal) { +func (tl *terminalLogger) BindToTerminal(t *Terminal) { tl.terminal = t } diff --git a/d2core/d2term/terminal_test.go b/d2core/d2term/terminal_test.go new file mode 100644 index 00000000..a2332f01 --- /dev/null +++ b/d2core/d2term/terminal_test.go @@ -0,0 +1,82 @@ +package d2term + +import ( + "fmt" + "testing" +) + +func TestTerminal(t *testing.T) { + term, err := NewTerminal() + if err != nil { + t.Fatal(err) + } + + lenOutput := len(term.outputHistory) + + const expected1 = 2 + if lenOutput != expected1 { + t.Fatalf("got %d expected %d", lenOutput, expected1) + } + + if err := term.Execute("clear"); err != nil { + t.Fatal(err) + } + + if err := term.Execute("ls"); err != nil { + t.Fatal(err) + } + + lenOutput = len(term.outputHistory) + + const expected2 = 3 + if lenOutput != expected2 { + t.Fatalf("got %d expected %d", lenOutput, expected2) + } +} + +func TestBind(t *testing.T) { + term, err := NewTerminal() + if err != nil { + t.Fatal(err) + } + + term.Clear() + + if err := term.Bind("hello", "world", []string{"world"}, func(args []string) error { + const expected = "world" + if args[0] != expected { + return fmt.Errorf("got %s expected %s", args[0], expected) + } + return nil + }); err != nil { + t.Fatal(err) + } + + if err := term.Execute("hello world"); err != nil { + t.Fatal(err) + } +} + +func TestUnbind(t *testing.T) { + term, err := NewTerminal() + if err != nil { + t.Fatal(err) + } + + if err := term.Unbind("clear"); err != nil { + t.Fatal(err) + } + + term.Clear() + + if err := term.Execute("ls"); err != nil { + t.Fatal(err) + } + + lenOutput := len(term.outputHistory) + + const expected = 2 + if lenOutput != expected { + t.Fatalf("got %d expected %d", lenOutput, expected) + } +} diff --git a/d2core/d2ui/button.go b/d2core/d2ui/button.go index cb61ebec..aad9c1c4 100644 --- a/d2core/d2ui/button.go +++ b/d2core/d2ui/button.go @@ -55,6 +55,7 @@ const ( ButtonTypeSquelchChat ButtonType = 35 ButtonTypeTabBlank ButtonType = 36 ButtonTypeBlankQuestBtn ButtonType = 37 + ButtonTypeAddSkill ButtonType = 38 ButtonNoFixedWidth int = -1 ButtonNoFixedHeight int = -1 @@ -180,7 +181,7 @@ const ( blankQuestButtonXSegments = 1 blankQuestButtonYSegments = 1 - blankQuestButtonDisabledFrames = -1 + blankQuestButtonDisabledFrames = 0 buttonMinipanelCharacterBaseFrame = 0 buttonMinipanelInventoryBaseFrame = 2 @@ -199,6 +200,10 @@ const ( buttonGoldCoinSegmentsY = 1 buttonGoldCoinDisabledFrame = -1 + buttonAddSkillSegmentsX = 1 + buttonAddSkillSegmentsY = 1 + buttonAddSkillDisabledFrame = 2 + pressedButtonOffset = 2 ) @@ -746,6 +751,20 @@ func getButtonLayouts() map[ButtonType]ButtonLayout { FixedHeight: ButtonNoFixedHeight, LabelColor: whiteAlpha100, }, + ButtonTypeAddSkill: { + XSegments: buttonAddSkillSegmentsX, + YSegments: buttonAddSkillSegmentsY, + DisabledFrame: buttonAddSkillDisabledFrame, + DisabledColor: whiteAlpha100, + ResourceName: d2resource.AddSkillButton, + PaletteName: d2resource.PaletteSky, + Toggleable: true, + FontPath: d2resource.Font16, + AllowFrameChange: true, + HasImage: true, + FixedWidth: ButtonNoFixedWidth, + FixedHeight: ButtonNoFixedHeight, + }, } } @@ -848,8 +867,7 @@ type buttonStateDescriptor struct { func (v *Button) createTooltip() { var t *Tooltip - // this is also related with https://github.com/OpenDiablo2/OpenDiablo2/issues/944 - // all strings starting with "#" could be wrong translated to another locales + switch v.buttonLayout.Tooltip { case buttonTooltipNone: return @@ -870,7 +888,7 @@ func (v *Button) createTooltip() { t.SetText(v.manager.asset.TranslateString("NPCRepairItems")) case buttonTooltipRepairAll: t = v.manager.NewTooltip(d2resource.Font16, d2resource.PaletteSky, TooltipXCenter, TooltipYBottom) - t.SetText(v.manager.asset.TranslateString("#128")) + t.SetText(v.manager.asset.TranslateLabel(d2enum.RepairAll)) case buttonTooltipLeftArrow: t = v.manager.NewTooltip(d2resource.Font16, d2resource.PaletteSky, TooltipXCenter, TooltipYBottom) t.SetText(v.manager.asset.TranslateString("KeyLeft")) @@ -905,7 +923,7 @@ func (v *Button) prerenderStates(btnSprite *Sprite, btnLayout *ButtonLayout, lab label.SetPosition(xOffset, textY) label.Render(v.normalSurface) - if !btnLayout.HasImage || !btnLayout.AllowFrameChange { + if !btnLayout.AllowFrameChange { return } @@ -1007,7 +1025,7 @@ func (v *Button) Render(target d2interface.Surface) { if v.toggled { target.Render(v.toggledSurface) - } else { + } else if v.buttonLayout.HasImage { // it allows to use SetEnabled(false) for non-image budons target.Render(v.disabledSurface) } case v.toggled && v.pressed: diff --git a/d2core/d2ui/checkbox.go b/d2core/d2ui/checkbox.go index c91bccbe..84984764 100644 --- a/d2core/d2ui/checkbox.go +++ b/d2core/d2ui/checkbox.go @@ -6,8 +6,8 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" ) -// static check that Checkbox implements Widget -var _ Widget = &Checkbox{} +// static check that Checkbox implements ClickableWidget +var _ ClickableWidget = &Checkbox{} // Checkbox represents a checkbox UI element type Checkbox struct { diff --git a/d2core/d2ui/frame.go b/d2core/d2ui/frame.go index 4d66c1cd..4255d61e 100644 --- a/d2core/d2ui/frame.go +++ b/d2core/d2ui/frame.go @@ -3,7 +3,7 @@ package d2ui import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" ) // static check that UIFrame implements Widget @@ -21,9 +21,9 @@ const ( // when it is visible. type UIFrame struct { *BaseWidget - asset *d2asset.AssetManager frame *Sprite frameOrientation frameOrientation + *d2util.Logger } // frame indices into dc6 images for panels @@ -41,11 +41,7 @@ const ( ) // NewUIFrame creates a new Frame instance -func NewUIFrame( - asset *d2asset.AssetManager, - uiManager *UIManager, - frameOrientation frameOrientation, -) *UIFrame { +func (ui *UIManager) NewUIFrame(frameOrientation frameOrientation) *UIFrame { var originX, originY = 0, 0 switch frameOrientation { @@ -57,32 +53,29 @@ func NewUIFrame( originY = 0 } - base := NewBaseWidget(uiManager) + base := NewBaseWidget(ui) + base.SetVisible(false) base.SetPosition(originX, originY) frame := &UIFrame{ BaseWidget: base, - asset: asset, frameOrientation: frameOrientation, + Logger: ui.Logger, } - frame.Load() - frame.asset.Logger.SetPrefix(logPrefix) // workaround + sprite, err := ui.NewSprite(d2resource.Frame, d2resource.PaletteSky) + if err != nil { + frame.Error(err.Error()) + } + + frame.frame = sprite + frame.calculateSize() + + ui.addWidget(frame) return frame } -// Load the necessary frame resources -func (u *UIFrame) Load() { - sprite, err := u.manager.NewSprite(d2resource.Frame, d2resource.PaletteSky) - if err != nil { - u.asset.Logger.Error(err.Error()) - } - - u.frame = sprite - u.calculateSize() -} - func (u *UIFrame) calculateSize() { var framesWidth, framesHeight []int @@ -111,7 +104,7 @@ func (u *UIFrame) calculateSize() { for i := range framesWidth { w, _, err := u.frame.GetFrameSize(framesWidth[i]) if err != nil { - u.asset.Logger.Error(err.Error()) + u.Error(err.Error()) } u.width += w @@ -120,7 +113,7 @@ func (u *UIFrame) calculateSize() { for i := range framesHeight { _, h, err := u.frame.GetFrameSize(framesHeight[i]) if err != nil { - u.asset.Logger.Error(err.Error()) + u.Error(err.Error()) } u.height += h @@ -132,11 +125,11 @@ func (u *UIFrame) Render(target d2interface.Surface) { switch u.frameOrientation { case FrameLeft: if err := u.renderLeft(target); err != nil { - u.asset.Logger.Error("Render error" + err.Error()) + u.Error("Render error" + err.Error()) } case FrameRight: if err := u.renderRight(target); err != nil { - u.asset.Logger.Error("Render error" + err.Error()) + u.Error("Render error" + err.Error()) } } } diff --git a/d2core/d2ui/label.go b/d2core/d2ui/label.go index 3582ed44..d151507d 100644 --- a/d2core/d2ui/label.go +++ b/d2core/d2ui/label.go @@ -11,6 +11,9 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" ) +// static check if Label implemented Widget +var _ Widget = &Label{} + // Label represents a user interface label type Label struct { *BaseWidget @@ -43,6 +46,10 @@ func (ui *UIManager) NewLabel(fontPath, palettePath string) *Label { result.bindManager(ui) + result.SetVisible(false) + + ui.addWidget(result) + return result } @@ -93,11 +100,6 @@ func (v *Label) Render(target d2interface.Surface) { target.Pop() } -// GetSize returns the size of the label -func (v *Label) GetSize() (width, height int) { - return v.font.GetTextMetrics(v.text) -} - // GetTextMetrics returns the width and height of the enclosing rectangle in Pixels. func (v *Label) GetTextMetrics(text string) (width, height int) { return v.font.GetTextMetrics(text) @@ -106,6 +108,12 @@ func (v *Label) GetTextMetrics(text string) (width, height int) { // SetText sets the label's text func (v *Label) SetText(newText string) { v.text = v.processColorTokens(newText) + v.BaseWidget.width, v.BaseWidget.height = v.font.GetTextMetrics(v.text) +} + +// GetText returns label text +func (v *Label) GetText() string { + return v.text } // SetBackgroundColor sets the background highlight color diff --git a/d2core/d2ui/label_button.go b/d2core/d2ui/label_button.go new file mode 100644 index 00000000..0c8c1e38 --- /dev/null +++ b/d2core/d2ui/label_button.go @@ -0,0 +1,121 @@ +package d2ui + +import ( + "image/color" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" +) + +const ( + white = 0xffffffff +) + +// static checks to ensure LabelButton implemented Widget and ClickableWidget +var _ Widget = &LabelButton{} +var _ ClickableWidget = &LabelButton{} + +// LabelButton represents LabelButton +type LabelButton struct { + *BaseWidget + stdColor color.Color + hoverColor color.Color + onClick func() + label *Label + + *d2util.Logger +} + +// NewLabelButton creates a label-button +func (ui *UIManager) NewLabelButton(font, palette string) *LabelButton { + base := NewBaseWidget(ui) + base.SetVisible(true) + + result := &LabelButton{ + BaseWidget: base, + stdColor: d2util.Color(white), + } + + result.label = ui.NewLabel(font, palette) + result.label.Alignment = HorizontalAlignCenter + result.label.Color[0] = result.stdColor + + ui.addWidget(result) + + return result +} + +// SetText sets button's text +func (b *LabelButton) SetText(text string) { + b.label.SetText(text) + b.width, b.height = b.label.GetSize() +} + +// SetColors sets label-button colors (on normal and hovered state) +func (b *LabelButton) SetColors(normColor, hoverColor color.Color) { + b.stdColor = normColor + b.hoverColor = hoverColor +} + +// GetSize returns label's size +func (b *LabelButton) GetSize() (x, y int) { + return b.label.GetSize() +} + +// GetPosition returns real position (including offset for the alignment) +func (b *LabelButton) GetPosition() (x, y int) { + return b.x - b.label.getAlignOffset(b.width), b.y +} + +// OnActivated defines the callback handler for the activate event +func (b *LabelButton) OnActivated(cb func()) { + b.onClick = cb +} + +// Activate calls the on activated callback handler, if any +func (b *LabelButton) Activate() { + if b.onClick == nil { + return + } + + b.onClick() +} + +// SetEnabled sets the enabled state +func (b *LabelButton) SetEnabled(_ bool) { + // noop +} + +// GetEnabled returns the enabled state +func (b *LabelButton) GetEnabled() bool { + return true +} + +// SetPressed sets the pressed state of the button +func (b *LabelButton) SetPressed(_ bool) { + // noop +} + +// GetPressed returns the pressed state of the button +func (b *LabelButton) GetPressed() bool { + return false +} + +// Advance advances the label-button +func (b *LabelButton) Advance(_ float64) error { + return nil +} + +// Render renders label-button +func (b *LabelButton) Render(target d2interface.Surface) { + target.PushTranslation(b.GetPosition()) + defer target.Pop() + + b.label.Render(target) + + if b.isHovered() { + b.label.Color[0] = b.hoverColor + } else { + b.label.Color[0] = b.stdColor + } +} diff --git a/d2core/d2ui/scrollbar.go b/d2core/d2ui/scrollbar.go index db41da5d..ba832019 100644 --- a/d2core/d2ui/scrollbar.go +++ b/d2core/d2ui/scrollbar.go @@ -13,8 +13,8 @@ const ( scrollbarWidth = 10 ) -// static check that Scrollbar implements widget -var _ Widget = &Scrollbar{} +// static check that Scrollbar implements clickable widget +var _ ClickableWidget = &Scrollbar{} // Scrollbar is a vertical slider ui element type Scrollbar struct { diff --git a/d2core/d2ui/sprite.go b/d2core/d2ui/sprite.go index 91988891..4a904add 100644 --- a/d2core/d2ui/sprite.go +++ b/d2core/d2ui/sprite.go @@ -11,6 +11,9 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" ) +// static check if Sprite implemented Widget +var _ Widget = &Sprite{} + // Sprite is a positioned visual object. type Sprite struct { *BaseWidget @@ -74,7 +77,7 @@ func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segments for x := 0; x < segmentsX; x++ { idx := x + y*segmentsX + frameOffset*segmentsX*segmentsY if err := s.animation.SetCurrentFrame(idx); err != nil { - s.Error("SetCurrentFrame error" + err.Error()) + s.Errorf("Error while setting frame (%d): %s", idx, err) } target.PushTranslation(s.x+currentX, s.y+currentY) diff --git a/d2core/d2ui/textbox.go b/d2core/d2ui/textbox.go index 9a874024..20ab092b 100644 --- a/d2core/d2ui/textbox.go +++ b/d2core/d2ui/textbox.go @@ -11,8 +11,8 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" ) -// static check that TextBox implements widget -var _ Widget = &TextBox{} +// static check that TextBox implements clickable widget +var _ ClickableWidget = &TextBox{} // TextBox represents a text input box type TextBox struct { diff --git a/d2core/d2ui/tooltip.go b/d2core/d2ui/tooltip.go index 281d0765..732efc63 100644 --- a/d2core/d2ui/tooltip.go +++ b/d2core/d2ui/tooltip.go @@ -69,6 +69,8 @@ func (ui *UIManager) NewTooltip(font, boxEnabled: true, } res.manager = ui + // cannot use ui.addWidget, because + // some tooltips could be covered by another widgets ui.addTooltip(res) return res diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index 552ea523..feecc61a 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -1,7 +1,6 @@ package d2gamescreen import ( - "image/color" "math" "os" "strconv" @@ -190,7 +189,7 @@ func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { v.characterNameLabel[i] = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.characterNameLabel[i].SetPosition(offsetX, offsetY) - v.characterNameLabel[i].Color[0] = rgbaColor(lightBrown) + v.characterNameLabel[i].Color[0] = d2util.Color(lightBrown) offsetY += labelHeight @@ -201,7 +200,7 @@ func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { v.characterExpLabel[i] = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic) v.characterExpLabel[i].SetPosition(offsetX, offsetY) - v.characterExpLabel[i].Color[0] = rgbaColor(lightGreen) + v.characterExpLabel[i].Color[0] = d2util.Color(lightGreen) } v.refreshGameStates() @@ -267,31 +266,6 @@ func (v *CharacterSelect) loadCharScrollbar() { v.charScrollbar.OnActivated(func() { v.onScrollUpdate() }) } -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 -} - func (v *CharacterSelect) createButtons(loading d2screen.LoadingState) { v.newCharButton = v.uiManager.NewButton(d2ui.ButtonTypeTall, strings.Join( d2util.SplitIntoLinesWithMaxWidth(v.asset.TranslateString("#831"), 13), "\n")) @@ -411,7 +385,7 @@ func (v *CharacterSelect) Render(screen d2interface.Surface) { } if v.showDeleteConfirmation { - screen.DrawRect(screenWidth, screenHeight, rgbaColor(blackHalfOpacity)) + screen.DrawRect(screenWidth, screenHeight, d2util.Color(blackHalfOpacity)) v.okCancelBox.RenderSegmented(screen, 2, 1, 0) v.deleteCharConfirmLabel.Render(screen) } diff --git a/d2game/d2gamescreen/cinematics.go b/d2game/d2gamescreen/cinematics.go index f1aa9a41..ee3171e3 100644 --- a/d2game/d2gamescreen/cinematics.go +++ b/d2game/d2gamescreen/cinematics.go @@ -98,7 +98,7 @@ func (v *Cinematics) OnLoad(_ d2screen.LoadingState) { v.cinematicsLabel = v.uiManager.NewLabel(d2resource.Font30, d2resource.PaletteStatic) v.cinematicsLabel.Alignment = d2ui.HorizontalAlignCenter v.cinematicsLabel.SetText(v.asset.TranslateLabel(d2enum.SelectCinematicLabel)) - v.cinematicsLabel.Color[0] = rgbaColor(lightBrown) + v.cinematicsLabel.Color[0] = d2util.Color(lightBrown) v.cinematicsLabel.SetPosition(cinematicsLabelX, cinematicsLabelY) } diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 07a826c1..4247ff36 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "image/color" + "strconv" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" @@ -130,58 +131,36 @@ type Game struct { func (v *Game) OnLoad(_ d2screen.LoadingState) { v.audioProvider.PlayBGM("") - err := v.terminal.BindAction( - "spawnitem", - "spawns an item at the local player position", - func(code1, code2, code3, code4, code5 string) { - codes := []string{code1, code2, code3, code4, code5} - v.debugSpawnItemAtPlayer(codes...) - }, - ) - if err != nil { - v.Errorf("failed to bind the '%s' action, err: %v\n", "spawnitem", err) + commands := []struct { + name string + desc string + args []string + fn func([]string) error + }{ + {"spawnitem", "spawns an item at the local player position", + []string{"code1", "code2", "code3", "code4", "code5"}, v.commandSpawnItem}, + {"spawnitemat", "spawns an item at the x,y coordinates", + []string{"x", "y", "code1", "code2", "code3", "code4", "code5"}, v.commandSpawnItemAt}, + {"spawnmon", "spawn monster at the local player position", []string{"name"}, v.commandSpawnMon}, } - err = v.terminal.BindAction( - "spawnitemat", - "spawns an item at the x,y coordinates", - func(x, y int, code1, code2, code3, code4, code5 string) { - codes := []string{code1, code2, code3, code4, code5} - v.debugSpawnItemAtLocation(x, y, codes...) - }, - ) - if err != nil { - v.Errorf("failed to bind the '%s' action, err: %v\n", "spawnitemat", err) + for _, cmd := range commands { + if err := v.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil { + v.Errorf(err.Error()) + } } - err = v.terminal.BindAction( - "spawnmon", - "spawn monster at the local player position", - func(name string) { - x := int(v.localPlayer.Position.X()) - y := int(v.localPlayer.Position.Y()) - monstat := v.asset.Records.Monster.Stats[name] - if monstat == nil { - v.terminal.OutputErrorf("no monstat entry for \"%s\"", name) - return - } - - monster, npcErr := v.gameClient.MapEngine.NewNPC(x, y, monstat, 0) - if npcErr != nil { - v.terminal.OutputErrorf("error generating monster \"%s\": %v", name, npcErr) - return - } - - v.gameClient.MapEngine.AddEntity(monster) - }, - ) - if err != nil { - v.Errorf("failed to bind the '%s' action, err: %v\n", "spawnmon", err) + if err := v.asset.BindTerminalCommands(v.terminal); err != nil { + v.Errorf(err.Error()) } } // OnUnload releases the resources of Gameplay screen func (v *Game) OnUnload() error { + if err := v.gameControls.UnbindTerminalCommands(v.terminal); err != nil { + return err + } + // https://github.com/OpenDiablo2/OpenDiablo2/issues/792 if err := v.inputManager.UnbindHandler(v.gameControls); err != nil { return err @@ -192,11 +171,7 @@ func (v *Game) OnUnload() error { return err } - if err := v.terminal.UnbindAction("spawnItemAt"); err != nil { - return err - } - - if err := v.terminal.UnbindAction("spawnItem"); err != nil { + if err := v.terminal.Unbind("spawnitemat", "spawnitem", "spawnmon"); err != nil { return err } @@ -208,6 +183,18 @@ func (v *Game) OnUnload() error { return err } + if err := v.asset.UnbindTerminalCommands(v.terminal); err != nil { + return err + } + + if err := v.mapRenderer.UnbindTerminalCommands(v.terminal); err != nil { + return err + } + + if err := v.soundEngine.UnbindTerminalCommands(v.terminal); err != nil { + return err + } + v.soundEngine.Reset() return nil @@ -304,7 +291,8 @@ func (v *Game) bindGameControls() error { var err error v.gameControls, err = d2player.NewGameControls(v.asset, v.renderer, player, v.gameClient.MapEngine, - v.escapeMenu, v.mapRenderer, v, v.terminal, v.uiManager, v.keyMap, v.logLevel, v.gameClient.IsSinglePlayer()) + v.escapeMenu, v.mapRenderer, v, v.terminal, v.uiManager, v.keyMap, v.audioProvider, v.logLevel, + v.gameClient.IsSinglePlayer()) if err != nil { return err @@ -394,3 +382,47 @@ func (v *Game) debugSpawnItemAtLocation(x, y int, codes ...string) { v.Errorf(spawnItemErrStr, x, y, codes) } } + +func (v *Game) commandSpawnItem(args []string) error { + v.debugSpawnItemAtPlayer(args...) + + return nil +} + +func (v *Game) commandSpawnItemAt(args []string) error { + x, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid argument") + } + + y, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid argument") + } + + v.debugSpawnItemAtLocation(x, y, args[2:]...) + + return nil +} + +func (v *Game) commandSpawnMon(args []string) error { + name := args[0] + x := int(v.localPlayer.Position.X()) + y := int(v.localPlayer.Position.Y()) + + monstat := v.asset.Records.Monster.Stats[name] + if monstat == nil { + v.terminal.Errorf("no monstat entry for \"%s\"", name) + return nil + } + + monster, npcErr := v.gameClient.MapEngine.NewNPC(x, y, monstat, 0) + if npcErr != nil { + v.terminal.Errorf("error generating monster \"%s\": %v", name, npcErr) + return nil + } + + v.gameClient.MapEngine.AddEntity(monster) + + return nil +} diff --git a/d2game/d2gamescreen/main_menu.go b/d2game/d2gamescreen/main_menu.go index 6a96355d..c11f8e09 100644 --- a/d2game/d2gamescreen/main_menu.go +++ b/d2game/d2gamescreen/main_menu.go @@ -62,6 +62,7 @@ const ( tcpJoinBtnX, tcpJoinBtnY = 264, 240 errorLabelX, errorLabelY = 400, 250 machineIPX, machineIPY = 400, 90 + tipX, tipY = 400, 300 ) const ( @@ -159,6 +160,8 @@ type MainMenu struct { tcpJoinGameLabel *d2ui.Label machineIP *d2ui.Label errorLabel *d2ui.Label + joinTipLabel *d2ui.Label + hostTipLabel *d2ui.Label tcpJoinGameEntry *d2ui.TextBox screenMode mainMenuScreenMode leftButtonHeld bool @@ -182,10 +185,12 @@ func (v *MainMenu) OnLoad(loading d2screen.LoadingState) { v.audioProvider.PlayBGM(d2resource.BGMTitle) loading.Progress(twentyPercent) - v.createLabels(loading) + v.createMainMenuLabels(loading) + v.createMultiplayerLabels() v.loadBackgroundSprites() v.createLogos(loading) - v.createButtons(loading) + v.createMainMenuButtons(loading) + v.createMultiplayerMenuButtons() v.tcpJoinGameEntry = v.uiManager.NewTextbox() v.tcpJoinGameEntry.SetPosition(joinGameDialogX, joinGameDialogY) @@ -235,39 +240,47 @@ func (v *MainMenu) loadBackgroundSprites() { v.serverIPBackground.SetPosition(serverIPbackgroundX, serverIPbackgroundY) } -func (v *MainMenu) createLabels(loading d2screen.LoadingState) { +func (v *MainMenu) createMainMenuLabels(loading d2screen.LoadingState) { v.versionLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.versionLabel.Alignment = d2ui.HorizontalAlignRight v.versionLabel.SetText("OpenDiablo2 - " + v.buildInfo.Branch) - v.versionLabel.Color[0] = rgbaColor(white) + v.versionLabel.Color[0] = d2util.Color(white) v.versionLabel.SetPosition(versionLabelX, versionLabelY) v.commitLabel = v.uiManager.NewLabel(d2resource.FontFormal10, d2resource.PaletteStatic) v.commitLabel.Alignment = d2ui.HorizontalAlignLeft v.commitLabel.SetText(v.buildInfo.Commit) - v.commitLabel.Color[0] = rgbaColor(white) + v.commitLabel.Color[0] = d2util.Color(white) v.commitLabel.SetPosition(commitLabelX, commitLabelY) v.copyrightLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.copyrightLabel.Alignment = d2ui.HorizontalAlignCenter v.copyrightLabel.SetText(v.asset.TranslateLabel(d2enum.CopyrightLabel)) - v.copyrightLabel.Color[0] = rgbaColor(lightBrown) + v.copyrightLabel.Color[0] = d2util.Color(lightBrown) v.copyrightLabel.SetPosition(copyrightX, copyrightY) loading.Progress(thirtyPercent) v.copyrightLabel2 = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.copyrightLabel2.Alignment = d2ui.HorizontalAlignCenter v.copyrightLabel2.SetText(v.asset.TranslateLabel(d2enum.AllRightsReservedLabel)) - v.copyrightLabel2.Color[0] = rgbaColor(lightBrown) + v.copyrightLabel2.Color[0] = d2util.Color(lightBrown) v.copyrightLabel2.SetPosition(copyright2X, copyright2Y) v.openDiabloLabel = v.uiManager.NewLabel(d2resource.FontFormal10, d2resource.PaletteStatic) v.openDiabloLabel.Alignment = d2ui.HorizontalAlignCenter v.openDiabloLabel.SetText("OpenDiablo2 is neither developed by, nor endorsed by Blizzard or its parent company Activision") - v.openDiabloLabel.Color[0] = rgbaColor(lightYellow) + v.openDiabloLabel.Color[0] = d2util.Color(lightYellow) v.openDiabloLabel.SetPosition(od2LabelX, od2LabelY) loading.Progress(fiftyPercent) + if v.errorLabel != nil { + v.errorLabel.SetPosition(errorLabelX, errorLabelY) + v.errorLabel.Alignment = d2ui.HorizontalAlignCenter + v.errorLabel.Color[0] = d2util.Color(red) + } +} + +func (v *MainMenu) createMultiplayerLabels() { v.tcpIPOptionsLabel = v.uiManager.NewLabel(d2resource.Font42, d2resource.PaletteUnits) v.tcpIPOptionsLabel.SetPosition(tcpOptionsX, tcpOptionsY) v.tcpIPOptionsLabel.Alignment = d2ui.HorizontalAlignCenter @@ -276,20 +289,30 @@ func (v *MainMenu) createLabels(loading d2screen.LoadingState) { v.tcpJoinGameLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.tcpJoinGameLabel.Alignment = d2ui.HorizontalAlignCenter v.tcpJoinGameLabel.SetText(strings.Join(d2util.SplitIntoLinesWithMaxWidth(v.asset.TranslateLabel(d2enum.TCPIPEnterHostIPLabel), 27), "\n")) - v.tcpJoinGameLabel.Color[0] = rgbaColor(gold) + v.tcpJoinGameLabel.Color[0] = d2util.Color(gold) v.tcpJoinGameLabel.SetPosition(joinGameX, joinGameY) v.machineIP = v.uiManager.NewLabel(d2resource.Font24, d2resource.PaletteUnits) v.machineIP.Alignment = d2ui.HorizontalAlignCenter v.machineIP.SetText(v.asset.TranslateLabel(d2enum.TCPIPYourIPLabel) + "\n" + v.getLocalIP()) - v.machineIP.Color[0] = rgbaColor(lightYellow) + v.machineIP.Color[0] = d2util.Color(lightYellow) v.machineIP.SetPosition(machineIPX, machineIPY) - if v.errorLabel != nil { - v.errorLabel.SetPosition(errorLabelX, errorLabelY) - v.errorLabel.Alignment = d2ui.HorizontalAlignCenter - v.errorLabel.Color[0] = rgbaColor(red) - } + v.hostTipLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteUnits) + v.hostTipLabel.Alignment = d2ui.HorizontalAlignCenter + v.hostTipLabel.SetText(d2ui.ColorTokenize(strings.Join(d2util.SplitIntoLinesWithMaxWidth( + v.asset.TranslateLabel(d2enum.TipHostLabel), 36), + "\n"), d2ui.ColorTokenGold)) + v.hostTipLabel.SetPosition(tipX, tipY) + v.hostTipLabel.SetVisible(false) + + v.joinTipLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteUnits) + v.joinTipLabel.Alignment = d2ui.HorizontalAlignCenter + v.joinTipLabel.SetText(d2ui.ColorTokenize(strings.Join(d2util.SplitIntoLinesWithMaxWidth( + v.asset.TranslateLabel(d2enum.TipJoinLabel), 36), + "\n"), d2ui.ColorTokenGold)) + v.joinTipLabel.SetPosition(tipX, tipY) + v.joinTipLabel.SetVisible(false) } func (v *MainMenu) createLogos(loading d2screen.LoadingState) { @@ -329,7 +352,7 @@ func (v *MainMenu) createLogos(loading d2screen.LoadingState) { v.diabloLogoRightBack.SetPosition(diabloLogoX, diabloLogoY) } -func (v *MainMenu) createButtons(loading d2screen.LoadingState) { +func (v *MainMenu) createMainMenuButtons(loading d2screen.LoadingState) { v.exitDiabloButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, v.asset.TranslateLabel(d2enum.ExitGameLabel)) v.exitDiabloButton.SetPosition(exitDiabloBtnX, exitDiabloBtnY) v.exitDiabloButton.OnActivated(func() { v.onExitButtonClicked() }) @@ -367,8 +390,6 @@ func (v *MainMenu) createButtons(loading d2screen.LoadingState) { v.btnServerIPOk = v.uiManager.NewButton(d2ui.ButtonTypeOkCancel, v.asset.TranslateString(d2enum.OKLabel)) v.btnServerIPOk.SetPosition(srvOkBtnX, srvOkBtnY) v.btnServerIPOk.OnActivated(func() { v.onBtnTCPIPOkClicked() }) - - v.createMultiplayerMenuButtons() loading.Progress(eightyPercent) } @@ -390,10 +411,14 @@ func (v *MainMenu) createMultiplayerMenuButtons() { v.btnTCPIPHostGame = v.uiManager.NewButton(d2ui.ButtonTypeWide, v.asset.TranslateLabel(d2enum.TCPIPHostGameLabel)) v.btnTCPIPHostGame.SetPosition(tcpHostBtnX, tcpHostBtnY) v.btnTCPIPHostGame.OnActivated(func() { v.onTCPIPHostGameClicked() }) + v.btnTCPIPHostGame.OnHoverStart(func() { v.hostTipLabel.SetVisible(true) }) + v.btnTCPIPHostGame.OnHoverEnd(func() { v.hostTipLabel.SetVisible(false) }) v.btnTCPIPJoinGame = v.uiManager.NewButton(d2ui.ButtonTypeWide, v.asset.TranslateLabel(d2enum.TCPIPJoinGameLabel)) v.btnTCPIPJoinGame.SetPosition(tcpJoinBtnX, tcpJoinBtnY) v.btnTCPIPJoinGame.OnActivated(func() { v.onTCPIPJoinGameClicked() }) + v.btnTCPIPJoinGame.OnHoverStart(func() { v.joinTipLabel.SetVisible(true) }) + v.btnTCPIPJoinGame.OnHoverEnd(func() { v.joinTipLabel.SetVisible(false) }) } func (v *MainMenu) onMapTestClicked() { diff --git a/d2game/d2player/escape_menu.go b/d2game/d2player/escape_menu.go index 2609da1f..0625add1 100644 --- a/d2game/d2player/escape_menu.go +++ b/d2game/d2player/escape_menu.go @@ -398,25 +398,15 @@ func (m *EscapeMenu) OnLoad() { // OnEscKey is called when the escape key is pressed func (m *EscapeMenu) OnEscKey() { - // note: original D2 returns straight to the game from however deep in the menu we are - switch m.currentLayout { - case optionsLayoutID: - m.setLayout(mainLayoutID) - return - case soundOptionsLayoutID, - videoOptionsLayoutID, - automapOptionsLayoutID, - configureControlsLayoutID: + if m.currentLayout == configureControlsLayoutID { m.setLayout(optionsLayoutID) if err := m.keyBindingMenu.Close(); err != nil { m.Errorf("unable to close the configure controls menu: %v", err.Error()) } - - return + } else { + m.close() } - - m.close() } // SetOnCloseCb sets the callback that is run when close() is called diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index c17f5281..8a4eb155 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -2,6 +2,7 @@ package d2player import ( "fmt" + "strconv" "strings" "time" @@ -27,7 +28,6 @@ const ( // Panel represents the panel at the bottom of the game screen type Panel interface { IsOpen() bool - Toggle() Open() Close() } @@ -37,11 +37,8 @@ const mouseBtnActionsThreshold = 0.25 const ( // Since they require special handling, not considering (1) globes, (2) content of the mini panel, (3) belt leftSkill actionableType = iota - newStats xp - walkRun stamina - newSkills rightSkill hpGlobe manaGlobe @@ -53,31 +50,16 @@ const ( leftSkillWidth, leftSkillHeight = 117, 550, 50, 50 - newStatsX, - newStatsY, - newStatsWidth, - newStatsHeight = 206, 563, 30, 30 - xpX, xpY, xpWidth, xpHeight = 253, 560, 125, 5 - walkRunX, - walkRunY, - walkRunWidth, - walkRunHeight = 255, 573, 17, 20 - staminaX, staminaY, staminaWidth, staminaHeight = 273, 573, 105, 20 - newSkillsX, - newSkillsY, - newSkillsWidth, - newSkillsHeight = 562, 563, 30, 30 - rightSkillX, rightSkillY, rightSkillWidth, @@ -124,6 +106,7 @@ func NewGameControls( term d2interface.Terminal, ui *d2ui.UIManager, keyMap *KeyMap, + audioProvider d2interface.AudioProvider, l d2util.LogLevel, isSinglePlayer bool, ) (*GameControls, error) { @@ -155,36 +138,18 @@ func NewGameControls( Width: leftSkillWidth, Height: leftSkillHeight, }}, - {newStats, d2geom.Rectangle{ - Left: newStatsX, - Top: newStatsY, - Width: newStatsWidth, - Height: newStatsHeight, - }}, {xp, d2geom.Rectangle{ Left: xpX, Top: xpY, Width: xpWidth, Height: xpHeight, }}, - {walkRun, d2geom.Rectangle{ - Left: walkRunX, - Top: walkRunY, - Width: walkRunWidth, - Height: walkRunHeight, - }}, {stamina, d2geom.Rectangle{ Left: staminaX, Top: staminaY, Width: staminaWidth, Height: staminaHeight, }}, - {newSkills, d2geom.Rectangle{ - Left: newSkillsX, - Top: newSkillsY, - Width: newSkillsWidth, - Height: newSkillsHeight, - }}, {rightSkill, d2geom.Rectangle{ Left: rightSkillX, Top: rightSkillY, @@ -207,14 +172,14 @@ func NewGameControls( inventoryRecord := asset.Records.Layout.Inventory[inventoryRecordKey] heroStatsPanel := NewHeroStatsPanel(asset, ui, hero.Name(), hero.Class, l, hero.Stats) - questLog := NewQuestLog(asset, ui, l, hero.Act) + questLog := NewQuestLog(asset, ui, l, audioProvider, hero.Act) inventory, err := NewInventory(asset, ui, l, hero.Gold, inventoryRecord) if err != nil { return nil, err } - skilltree := newSkillTree(hero.Skills, hero.Class, asset, l, ui) + skilltree := newSkillTree(hero.Skills, hero.Class, hero.Stats, asset, l, ui) miniPanel := newMiniPanel(asset, ui, l, isSinglePlayer) @@ -224,13 +189,9 @@ func NewGameControls( } helpOverlay := NewHelpOverlay(asset, ui, l, keyMap) - hud := NewHUD(asset, ui, hero, miniPanel, actionableRegions, mapEngine, l, mapRenderer) const blackAlpha50percent = 0x0000007f - hoverLabel := hud.nameLabel - hoverLabel.SetBackgroundColor(d2util.Color(blackAlpha50percent)) - gc := &GameControls{ asset: asset, ui: ui, @@ -246,7 +207,6 @@ func NewGameControls( questLog: questLog, HelpOverlay: helpOverlay, keyMap: keyMap, - hud: hud, bottomMenuRect: &d2geom.Rectangle{ Left: menuBottomRectX, Top: menuBottomRectY, @@ -271,6 +231,12 @@ func NewGameControls( isSinglePlayer: isSinglePlayer, } + hud := NewHUD(asset, ui, hero, miniPanel, actionableRegions, mapEngine, l, gc, mapRenderer) + gc.hud = hud + + hoverLabel := hud.nameLabel + hoverLabel.SetBackgroundColor(d2util.Color(blackAlpha50percent)) + gc.heroStatsPanel.SetOnCloseCb(gc.onCloseHeroStatsPanel) gc.questLog.SetOnCloseCb(gc.onCloseQuestLog) gc.inventory.SetOnCloseCb(gc.onCloseInventory) @@ -389,11 +355,7 @@ func (g *GameControls) OnKeyDown(event d2interface.KeyEvent) bool { switch gameEvent { case d2enum.ClearScreen: - g.inventory.Close() - g.skilltree.Close() - g.heroStatsPanel.Close() - g.questLog.Close() - g.HelpOverlay.Close() + g.clearScreen() g.updateLayout() case d2enum.ToggleInventoryPanel: g.toggleInventoryPanel() @@ -420,11 +382,8 @@ func (g *GameControls) OnKeyDown(event d2interface.KeyEvent) bool { func (g *GameControls) OnKeyUp(event d2interface.KeyEvent) bool { gameEvent := g.keyMap.getGameEvent(event.Key()) - switch gameEvent { - case d2enum.HoldRun: + if gameEvent == d2enum.HoldRun { g.hud.onToggleRunButton(true) - default: - return false } return false @@ -437,57 +396,18 @@ func (g *GameControls) OnKeyUp(event d2interface.KeyEvent) bool { func (g *GameControls) onEscKey() { escHandled := false - if g.hud.skillSelectMenu.IsOpen() { - g.hud.skillSelectMenu.ClosePanels() + escHandled = g.hasOpenPanels() || g.HelpOverlay.IsOpen() || g.hud.skillSelectMenu.IsOpen() + g.clearScreen() - escHandled = true - } - - if g.inventory.IsOpen() { - if g.inventory.moveGoldPanel.IsOpen() { - g.inventory.moveGoldPanel.Close() - - return - } - - g.inventory.Close() - - escHandled = true - } - - if g.skilltree.IsOpen() { - g.skilltree.Close() - - escHandled = true - } - - if g.heroStatsPanel.IsOpen() { - g.heroStatsPanel.Close() - - escHandled = true - } - - if g.questLog.IsOpen() { - g.questLog.Close() - - escHandled = true - } - - if g.HelpOverlay.IsOpen() { - g.HelpOverlay.Close() - - escHandled = true - } - - switch escHandled { - case true: + if escHandled { g.updateLayout() - case false: - if g.escapeMenu.IsOpen() { - g.escapeMenu.OnEscKey() - } else { - g.openEscMenu() - } + return + } + + if g.escapeMenu.IsOpen() { + g.escapeMenu.OnEscKey() + } else { + g.openEscMenu() } } @@ -629,36 +549,89 @@ func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool { return false } -func (g *GameControls) toggleHeroStatsPanel() { +func (g *GameControls) clearLeftScreenSide() { + g.heroStatsPanel.Close() + g.questLog.Close() + g.hud.skillSelectMenu.ClosePanels() + g.hud.miniPanel.SetMovedRight(false) + g.updateLayout() +} + +func (g *GameControls) clearRightScreenSide() { + g.inventory.Close() + g.skilltree.Close() + g.hud.skillSelectMenu.ClosePanels() + g.hud.miniPanel.SetMovedLeft(false) + g.updateLayout() +} + +func (g *GameControls) clearScreen() { + g.clearRightScreenSide() + g.clearLeftScreenSide() + g.hud.skillSelectMenu.ClosePanels() + g.HelpOverlay.Close() +} + +func (g *GameControls) openLeftPanel(panel Panel) { if !g.HelpOverlay.IsOpen() { - g.questLog.Close() - g.heroStatsPanel.Toggle() - g.hud.miniPanel.SetMovedRight(g.heroStatsPanel.IsOpen()) - g.updateLayout() + isOpen := panel.IsOpen() + + g.clearLeftScreenSide() + + if !isOpen { + panel.Open() + g.hud.miniPanel.SetMovedRight(true) + g.updateLayout() + } } } +func (g *GameControls) openRightPanel(panel Panel) { + if !g.HelpOverlay.IsOpen() { + isOpen := panel.IsOpen() + + g.clearRightScreenSide() + + if !isOpen { + panel.Open() + g.hud.miniPanel.SetMovedLeft(true) + g.updateLayout() + } + } +} + +func (g *GameControls) toggleHeroStatsPanel() { + g.openLeftPanel(g.heroStatsPanel) +} + func (g *GameControls) onCloseHeroStatsPanel() { - g.hud.miniPanel.SetMovedRight(g.heroStatsPanel.IsOpen()) - g.updateLayout() } -func (g *GameControls) toggleQuestLog() { +func (g *GameControls) toggleLeftSkillPanel() { if !g.HelpOverlay.IsOpen() { - g.heroStatsPanel.Close() - g.questLog.Toggle() - g.hud.miniPanel.SetMovedRight(g.questLog.IsOpen()) - g.updateLayout() + g.clearScreen() + g.hud.skillSelectMenu.ToggleLeftPanel() } } +func (g *GameControls) toggleRightSkillPanel() { + if !g.HelpOverlay.IsOpen() { + g.clearScreen() + g.hud.skillSelectMenu.ToggleRightPanel() + } +} + +func (g *GameControls) toggleQuestLog() { + g.openLeftPanel(g.questLog) +} + func (g *GameControls) onCloseQuestLog() { - g.hud.miniPanel.SetMovedRight(g.questLog.IsOpen()) - g.updateLayout() } func (g *GameControls) toggleHelpOverlay() { - if !g.inventory.IsOpen() && !g.skilltree.IsOpen() && !g.heroStatsPanel.IsOpen() && !g.questLog.IsOpen() { + if !g.isRightPanelOpen() || g.isLeftPanelOpen() { + g.HelpOverlay.updateKeyMap(g.keyMap) + g.hud.skillSelectMenu.ClosePanels() g.hud.miniPanel.openDisabled() g.HelpOverlay.Toggle() g.updateLayout() @@ -666,38 +639,21 @@ func (g *GameControls) toggleHelpOverlay() { } func (g *GameControls) toggleInventoryPanel() { - if !g.HelpOverlay.IsOpen() { - g.skilltree.Close() - g.inventory.Toggle() - g.hud.miniPanel.SetMovedLeft(g.inventory.IsOpen()) - g.updateLayout() - } + g.openRightPanel(g.inventory) } func (g *GameControls) onCloseInventory() { - g.hud.miniPanel.SetMovedLeft(g.inventory.IsOpen()) - g.updateLayout() } func (g *GameControls) toggleSkilltreePanel() { - if !g.HelpOverlay.IsOpen() { - g.inventory.Close() - g.skilltree.Toggle() - g.hud.miniPanel.SetMovedLeft(g.skilltree.IsOpen()) - g.updateLayout() - } + g.openRightPanel(g.skilltree) } func (g *GameControls) onCloseSkilltree() { - g.hud.miniPanel.SetMovedLeft(g.skilltree.IsOpen()) - g.updateLayout() } func (g *GameControls) openEscMenu() { - g.inventory.Close() - g.skilltree.Close() - g.heroStatsPanel.Close() - g.questLog.Close() + g.clearScreen() g.hud.miniPanel.closeDisabled() g.escapeMenu.open() g.updateLayout() @@ -712,6 +668,9 @@ func (g *GameControls) Load() { g.questLog.Load() g.HelpOverlay.Load() + g.loadAddButtons() + g.setAddButtons() + miniPanelActions := &miniPanelActions{ characterToggle: g.toggleHeroStatsPanel, inventoryToggle: g.toggleInventoryPanel, @@ -727,11 +686,16 @@ func (g *GameControls) Advance(elapsed float64) error { g.mapRenderer.Advance(elapsed) g.hud.Advance(elapsed) g.inventory.Advance(elapsed) + g.questLog.Advance(elapsed) if err := g.escapeMenu.Advance(elapsed); err != nil { return err } + if g.heroStatsPanel.IsOpen() || g.skilltree.IsOpen() { + g.setAddButtons() + } + return nil } @@ -744,13 +708,12 @@ func (g *GameControls) updateLayout() { g.mapRenderer.ViewportDefault() case isRightPanelOpen: g.mapRenderer.ViewportToLeft() - default: + case isLeftPanelOpen: g.mapRenderer.ViewportToRight() } } func (g *GameControls) isLeftPanelOpen() bool { - // https://github.com/OpenDiablo2/OpenDiablo2/issues/801 return g.heroStatsPanel.IsOpen() || g.questLog.IsOpen() || g.inventory.moveGoldPanel.IsOpen() } @@ -758,6 +721,10 @@ func (g *GameControls) isRightPanelOpen() bool { return g.inventory.IsOpen() || g.skilltree.IsOpen() } +func (g *GameControls) hasOpenPanels() bool { + return g.isRightPanelOpen() || g.isLeftPanelOpen() || g.hud.skillSelectMenu.IsOpen() +} + func (g *GameControls) isInActiveMenusRect(px, py int) bool { if g.bottomMenuRect.IsInRect(px, py) { return true @@ -792,11 +759,11 @@ func (g *GameControls) isInActiveMenusRect(px, py int) bool { // Render draws the GameControls onto the target func (g *GameControls) Render(target d2interface.Surface) error { - if err := g.renderPanels(target); err != nil { + if err := g.hud.Render(target); err != nil { return err } - if err := g.hud.Render(target); err != nil { + if err := g.renderPanels(target); err != nil { return err } @@ -854,11 +821,8 @@ func (g *GameControls) ToggleManaStats() { func (g *GameControls) onHoverActionable(item actionableType) { hoverMap := map[actionableType]func(){ leftSkill: func() {}, - newStats: func() {}, xp: func() {}, - walkRun: func() {}, stamina: func() {}, - newSkills: func() {}, rightSkill: func() {}, hpGlobe: func() {}, manaGlobe: func() {}, @@ -877,31 +841,19 @@ func (g *GameControls) onHoverActionable(item actionableType) { func (g *GameControls) onClickActionable(item actionableType) { actionMap := map[actionableType]func(){ leftSkill: func() { - g.hud.skillSelectMenu.ToggleLeftPanel() - }, - - newStats: func() { - g.Info("New Stats Selector Action Pressed") + g.toggleLeftSkillPanel() }, xp: func() { g.Info("XP Action Pressed") }, - walkRun: func() { - g.Info("Walk/Run Action Pressed") - }, - stamina: func() { g.Info("Stamina Action Pressed") }, - newSkills: func() { - g.Info("New Skills Selector Action Pressed") - }, - rightSkill: func() { - g.hud.skillSelectMenu.ToggleRightPanel() + g.toggleRightSkillPanel() }, hpGlobe: func() { @@ -925,59 +877,135 @@ func (g *GameControls) onClickActionable(item actionableType) { action() } -func (g *GameControls) bindFreeCamCommand(term d2interface.Terminal) error { - return term.BindAction("freecam", "toggle free camera movement", func() { - g.FreeCam = !g.FreeCam - }) +func (g *GameControls) bindTerminalCommands(term d2interface.Terminal) error { + if err := term.Bind("freecam", "toggle free camera movement", nil, g.commandFreeCam); err != nil { + return err + } + + if err := term.Bind("setleftskill", "set skill to fire on left click", []string{"id"}, g.commandSetLeftSkill(term)); err != nil { + return err + } + + if err := term.Bind("setrightskill", "set skill to fire on right click", []string{"id"}, g.commandSetRightSkill(term)); err != nil { + return err + } + + if err := term.Bind("learnskills", "learn all skills for the a given class", []string{"token"}, g.commandLearnSkills(term)); err != nil { + return err + } + + if err := term.Bind("learnskillid", "learn a skill by a given ID", []string{"id"}, g.commandLearnSkillID(term)); err != nil { + return err + } + + return nil } -func (g *GameControls) bindSetLeftSkillCommand(term d2interface.Terminal) error { - setLeftSkill := func(id int) { - skillRecord := g.asset.Records.Skill.Details[id] - skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) +// UnbindTerminalCommands unbinds commands from the terminal +func (g *GameControls) UnbindTerminalCommands(term d2interface.Terminal) error { + return term.Unbind("freecam", "setleftskill", "setrightskill", "learnskills", "learnskillid") +} +func (g *GameControls) setAddButtons() { + g.hud.addStatsButton.SetEnabled(g.hero.Stats.StatsPoints > 0) + g.hud.addSkillButton.SetEnabled(g.hero.Stats.SkillPoints > 0) +} + +func (g *GameControls) loadAddButtons() { + g.hud.addStatsButton.OnActivated(func() { g.toggleHeroStatsPanel() }) + g.hud.addSkillButton.OnActivated(func() { g.toggleSkilltreePanel() }) +} + +func (g *GameControls) commandFreeCam([]string) error { + g.FreeCam = !g.FreeCam + + return nil +} + +func (g *GameControls) commandSetLeftSkill(term d2interface.Terminal) func(args []string) error { + return func(args []string) error { + id, err := strconv.Atoi(args[0]) if err != nil { - term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) - return + term.Errorf("invalid argument") + return nil + } + + skill, err := g.heroSkillByID(id) + if err != nil { + term.Errorf(err.Error()) + return nil } g.hero.LeftSkill = skill - } - return term.BindAction( - "setleftskill", - "set skill to fire on left click", - setLeftSkill, - ) + return nil + } } -func (g *GameControls) bindSetRightSkillCommand(term d2interface.Terminal) error { - setRightSkill := func(id int) { - skillRecord := g.asset.Records.Skill.Details[id] - skill, err := g.heroState.CreateHeroSkill(0, skillRecord.Skill) - +func (g *GameControls) commandSetRightSkill(term d2interface.Terminal) func(args []string) error { + return func(args []string) error { + id, err := strconv.Atoi(args[0]) if err != nil { - term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) - return + term.Errorf("invalid argument") + return nil + } + + skill, err := g.heroSkillByID(id) + if err != nil { + term.Errorf(err.Error()) + return nil } g.hero.RightSkill = skill - } - return term.BindAction( - "setrightskill", - "set skill to fire on right click", - setRightSkill, - ) + return nil + } } -const classTokenLength = 3 +func (g *GameControls) commandLearnSkillID(term d2interface.Terminal) func(args []string) error { + return func(args []string) error { + id, err := strconv.Atoi(args[0]) + if err != nil { + term.Errorf("invalid argument") + return nil + } -func (g *GameControls) bindLearnSkillsCommand(term d2interface.Terminal) error { - learnSkills := func(token string) { + skill, err := g.heroSkillByID(id) + if err != nil { + term.Errorf(err.Error()) + return nil + } + + g.hero.Skills[skill.ID] = skill + g.hud.skillSelectMenu.RegenerateImageCache() + g.Infof("Learned skill: " + skill.Skill) + + return nil + } +} + +func (g *GameControls) heroSkillByID(id int) (*d2hero.HeroSkill, error) { + skillRecord := g.asset.Records.Skill.Details[id] + if skillRecord == nil { + return nil, fmt.Errorf("cannot find a skill record for ID: %d", id) + } + + skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) + if err != nil { + return nil, fmt.Errorf("cannot create skill with ID of %d", id) + } + + return skill, nil +} + +func (g *GameControls) commandLearnSkills(term d2interface.Terminal) func(args []string) error { + const classTokenLength = 3 + + return func(args []string) error { + token := args[0] if len(token) < classTokenLength { - term.OutputErrorf("The given class token should be at least 3 characters") - return + term.Errorf("The given class token should be at least 3 characters") + return nil } validPrefixes := []string{"ama", "ass", "nec", "bar", "sor", "dru", "pal"} @@ -993,9 +1021,9 @@ func (g *GameControls) bindLearnSkillsCommand(term d2interface.Terminal) error { if !isValidToken { fmtInvalid := "Invalid class, must be a value starting with(case insensitive): %s" - term.OutputErrorf(fmtInvalid, strings.Join(validPrefixes, ", ")) + term.Errorf(fmtInvalid, strings.Join(validPrefixes, ", ")) - return + return nil } var err error @@ -1031,70 +1059,10 @@ func (g *GameControls) bindLearnSkillsCommand(term d2interface.Terminal) error { g.Infof("Learned %d skills", learnedSkillsCount) if err != nil { - term.OutputErrorf("cannot learn skill for class, error: %s", err) - return + term.Errorf("cannot learn skill for class, error: %s", err) + return nil } - } - return term.BindAction( - "learnskills", - "learn all skills for the a given class", - learnSkills, - ) -} - -func (g *GameControls) bindLearnSkillByIDCommand(term d2interface.Terminal) error { - learnByID := func(id int) { - skillRecord := g.asset.Records.Skill.Details[id] - if skillRecord == nil { - term.OutputErrorf("cannot find a skill record for ID: %d", id) - return - } - - skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) - if skill == nil { - term.OutputErrorf("cannot create skill: %s", skillRecord.Skill) - return - } - - g.hero.Skills[skill.ID] = skill - - if err != nil { - term.OutputErrorf("cannot learn skill for class, error: %s", err) - return - } - - g.hud.skillSelectMenu.RegenerateImageCache() - g.Info("Learned skill: " + skill.Skill) - } - - return term.BindAction( - "learnskillid", - "learn a skill by a given ID", - learnByID, - ) -} - -func (g *GameControls) bindTerminalCommands(term d2interface.Terminal) error { - if err := g.bindFreeCamCommand(term); err != nil { - return err - } - - if err := g.bindSetLeftSkillCommand(term); err != nil { - return err - } - - if err := g.bindSetRightSkillCommand(term); err != nil { - return err - } - - if err := g.bindLearnSkillsCommand(term); err != nil { - return err - } - - if err := g.bindLearnSkillByIDCommand(term); err != nil { - return err - } - - return nil + return nil + } } diff --git a/d2game/d2player/globeWidget.go b/d2game/d2player/globeWidget.go index f2bccb84..6a8da540 100644 --- a/d2game/d2player/globeWidget.go +++ b/d2game/d2player/globeWidget.go @@ -68,7 +68,7 @@ func newGlobeWidget(ui *d2ui.UIManager, asset *d2asset.AssetManager, x, y int, gtype globeType, - value *int, valueMax *int, + value, valueMax *int, l d2util.LogLevel) *globeWidget { var globe, overlap *globeFrame diff --git a/d2game/d2player/help_overlay.go b/d2game/d2player/help_overlay.go index 54ab30b6..07af8aff 100644 --- a/d2game/d2player/help_overlay.go +++ b/d2game/d2player/help_overlay.go @@ -155,6 +155,8 @@ const ( beltDotY = 568 ) +const bullets = 8 + // NewHelpOverlay creates a new HelpOverlay instance func NewHelpOverlay( asset *d2asset.AssetManager, @@ -182,6 +184,7 @@ type HelpOverlay struct { frames []*d2ui.Sprite text []*d2ui.Label lines []line + bullets [bullets]*d2ui.Label uiManager *d2ui.UIManager closeButton *d2ui.Button keyMap *KeyMap @@ -332,11 +335,8 @@ func (h *HelpOverlay) setupTitleAndButton() { h.text = append(h.text, newLabel) } -func (h *HelpOverlay) setupBulletedList() { +func (h *HelpOverlay) updateBulletText() { // Bullets - // the hotkeys displayed here should be pulled from a mapping of input events to game events - // https://github.com/OpenDiablo2/OpenDiablo2/issues/793 - // https://github.com/OpenDiablo2/OpenDiablo2/issues/794 callouts := []struct{ text string }{ // "Ctrl" should be hotkey // "Hold Down <%s> to Run" {text: fmt.Sprintf( @@ -378,17 +378,35 @@ func (h *HelpOverlay) setupBulletedList() { )}, } - for idx := range callouts { + for i := 0; i < bullets; i++ { + h.bullets[i].SetText(callouts[i].text) + } +} + +func (h *HelpOverlay) setupBulletedList() { + for idx := 0; idx < bullets; idx++ { listItemOffsetY := idx * listItemVerticalOffset - h.createBullet(callout{ - LabelText: callouts[idx].text, - LabelX: listRootX, - LabelY: listRootY + listItemOffsetY, - DotX: listBulletX, - DotY: listBulletRootY + listItemOffsetY, - }) + label := h.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteSky) + label.SetPosition(listRootX, listRootY+listItemOffsetY) + h.bullets[idx] = label + h.panelGroup.AddWidget(h.bullets[idx]) + + newDot, err := h.uiManager.NewSprite(d2resource.HelpYellowBullet, d2resource.PaletteSky) + if err != nil { + h.Error(err.Error()) + } + + err = newDot.SetCurrentFrame(0) + if err != nil { + h.Error(err.Error()) + } + + newDot.SetPosition(listBulletX, listBulletRootY+listItemOffsetY+bulletOffsetY) + + h.frames = append(h.frames, newDot) } + h.updateBulletText() } // nolint:funlen // can't reduce @@ -409,7 +427,7 @@ func (h *HelpOverlay) setupLabelsWithLines() { DotY: newSkillDotY, }) - // Some of the help fonts require mulktiple lines. + // Some of the help fonts require multiple lines. h.createLabel(callout{ LabelText: h.asset.TranslateString("StrHelp10"), // "Left Mouse-" LabelX: leftMouseLabelX, @@ -553,26 +571,6 @@ type callout struct { DotY int } -func (h *HelpOverlay) createBullet(c callout) { - newLabel := h.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteSky) - newLabel.SetText(c.LabelText) - newLabel.SetPosition(c.LabelX, c.LabelY) - h.text = append(h.text, newLabel) - - newDot, err := h.uiManager.NewSprite(d2resource.HelpYellowBullet, d2resource.PaletteSky) - if err != nil { - h.Error(err.Error()) - } - - err = newDot.SetCurrentFrame(0) - if err != nil { - h.Error(err.Error()) - } - - newDot.SetPosition(c.DotX, c.DotY+bulletOffsetY) - h.frames = append(h.frames, newDot) -} - func (h *HelpOverlay) createLabel(c callout) { newLabel := h.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteSky) newLabel.SetText(c.LabelText) @@ -631,3 +629,8 @@ func (h *HelpOverlay) Render(target d2interface.Surface) { target.Pop() } } + +func (h *HelpOverlay) updateKeyMap(km *KeyMap) { + h.keyMap = km + h.updateBulletText() +} diff --git a/d2game/d2player/hero_stats_panel.go b/d2game/d2player/hero_stats_panel.go index 9f9cb5c0..759e2451 100644 --- a/d2game/d2player/hero_stats_panel.go +++ b/d2game/d2player/hero_stats_panel.go @@ -9,6 +9,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) @@ -55,6 +56,15 @@ const ( const ( heroStatsCloseButtonX, heroStatsCloseButtonY = 208, 453 + addStatSocketOffsetX, addStatSocketOffsetY = -3, 34 +) + +const ( + newStatsRemainingPointsFieldX, newStatsRemainingPointsFieldY = 83, 430 + newStatsRemainingPointsLabelX = 92 + newStatsRemainingPointsLabel1Y = 411 + newStatsRemainingPointsLabel2Y = 418 + newStatsRemainingPointsValueX, newStatsRemainingPointsValueY = 188, 411 ) // PanelText represents text on the panel @@ -113,15 +123,17 @@ func NewHeroStatsPanel(asset *d2asset.AssetManager, // HeroStatsPanel represents the hero status panel type HeroStatsPanel struct { - asset *d2asset.AssetManager - uiManager *d2ui.UIManager - panel *d2ui.Sprite - heroState *d2hero.HeroStatsState - heroName string - heroClass d2enum.Hero - labels *StatsPanelLabels - onCloseCb func() - panelGroup *d2ui.WidgetGroup + asset *d2asset.AssetManager + uiManager *d2ui.UIManager + panel *d2ui.Sprite + heroState *d2hero.HeroStatsState + heroName string + heroClass d2enum.Hero + labels *StatsPanelLabels + onCloseCb func() + panelGroup *d2ui.WidgetGroup + newStatPoints *d2ui.WidgetGroup + remainingPoints *d2ui.Label originX int originY int @@ -135,8 +147,9 @@ func (s *HeroStatsPanel) Load() { var err error s.panelGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityHeroStatsPanel) + s.newStatPoints = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityHeroStatsPanel) - frame := d2ui.NewUIFrame(s.asset, s.uiManager, d2ui.FrameLeft) + frame := s.uiManager.NewUIFrame(d2ui.FrameLeft) s.panelGroup.AddWidget(frame) s.panel, err = s.uiManager.NewSprite(d2resource.InventoryCharacterPanel, d2resource.PaletteSky) @@ -154,10 +167,91 @@ func (s *HeroStatsPanel) Load() { closeButton.OnActivated(func() { s.Close() }) s.panelGroup.AddWidget(closeButton) + s.loadNewStatPoints() + s.setLayout() + s.initStatValueLabels() s.panelGroup.SetVisible(false) } +func (s *HeroStatsPanel) loadNewStatPoints() { + field, err := s.uiManager.NewSprite(d2resource.HeroStatsPanelStatsPoints, d2resource.PaletteSky) + if err != nil { + s.Error(err.Error()) + } + + field.SetPosition(newStatsRemainingPointsFieldX, newStatsRemainingPointsFieldY) + s.newStatPoints.AddWidget(field) + + label1 := s.uiManager.NewLabel(d2resource.Font6, d2resource.PaletteSky) + label1.SetPosition(newStatsRemainingPointsLabelX, newStatsRemainingPointsLabel1Y) + label1.SetText(s.asset.TranslateString("strchrstat")) + label1.Color[0] = d2util.Color(d2gui.ColorRed) + s.newStatPoints.AddWidget(label1) + + label2 := s.uiManager.NewLabel(d2resource.Font6, d2resource.PaletteSky) + label2.SetPosition(newStatsRemainingPointsLabelX, newStatsRemainingPointsLabel2Y) + label2.SetText(s.asset.TranslateString("strchrrema")) + label2.Color[0] = d2util.Color(d2gui.ColorRed) + s.newStatPoints.AddWidget(label2) + + s.remainingPoints = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteSky) + s.remainingPoints.SetText(strconv.Itoa(s.heroState.StatsPoints)) + s.remainingPoints.SetPosition(newStatsRemainingPointsValueX, newStatsRemainingPointsValueY) + s.remainingPoints.Alignment = d2ui.HorizontalAlignCenter + s.newStatPoints.AddWidget(s.remainingPoints) + + buttons := []struct { + x int + y int + cb func() + }{ + {205, 140, func() { + s.heroState.Strength++ + }}, + {205, 201, func() { + s.heroState.Dexterity++ + }}, + {205, 286, func() { + s.heroState.Vitality++ + }}, + {205, 347, func() { + s.heroState.Energy++ + }}, + } + + var socket *d2ui.Sprite + + var button *d2ui.Button + + for _, i := range buttons { + currentValue := i + + socket, err = s.uiManager.NewSprite(d2resource.HeroStatsPanelSocket, d2resource.PaletteSky) + if err != nil { + s.Error(err.Error()) + } + + socket.SetPosition(i.x+addStatSocketOffsetX, i.y+addStatSocketOffsetY) + s.newStatPoints.AddWidget(socket) + + button = s.uiManager.NewButton(d2ui.ButtonTypeAddSkill, d2resource.PaletteSky) + button.SetPosition(i.x, i.y) + button.OnActivated(func() { + currentValue.cb() + s.heroState.StatsPoints-- + s.remainingPoints.SetText(strconv.Itoa(s.heroState.StatsPoints)) + s.setStatValues() + s.setLayout() + }) + s.newStatPoints.AddWidget(button) + } +} + +func (s *HeroStatsPanel) setLayout() { + s.newStatPoints.SetVisible(s.heroState.StatsPoints > 0 && s.IsOpen()) +} + // IsOpen returns true if the hero status panel is open func (s *HeroStatsPanel) IsOpen() bool { return s.isOpen @@ -176,12 +270,14 @@ func (s *HeroStatsPanel) Toggle() { func (s *HeroStatsPanel) Open() { s.isOpen = true s.panelGroup.SetVisible(true) + s.setLayout() } // Close closed the hero status panel func (s *HeroStatsPanel) Close() { s.isOpen = false s.panelGroup.SetVisible(false) + s.setLayout() s.onCloseCb() } diff --git a/d2game/d2player/hud.go b/d2game/d2player/hud.go index be455a3f..e68608c7 100644 --- a/d2game/d2player/hud.go +++ b/d2game/d2player/hud.go @@ -70,6 +70,11 @@ const ( whiteAlpha100 = 0xffffffff ) +const ( + addStatsButtonX, addStatsButtonY = 206, 561 + addSkillButtonX, addSkillButtonY = 563, 561 +) + // HUD represents the always visible user interface of the game type HUD struct { actionableRegions []actionableRegion @@ -103,7 +108,11 @@ type HUD struct { widgetLeftSkill *d2ui.CustomWidget widgetRightSkill *d2ui.CustomWidget panelBackground *d2ui.CustomWidget + addStatsButton *d2ui.Button + addSkillButton *d2ui.Button panelGroup *d2ui.WidgetGroup + gameControls *GameControls + *d2util.Logger } @@ -116,6 +125,7 @@ func NewHUD( actionableRegions []actionableRegion, mapEngine *d2mapengine.MapEngine, l d2util.LogLevel, + gameControls *GameControls, mapRenderer *d2maprenderer.MapRenderer, ) *HUD { nameLabel := ui.NewLabel(d2resource.Font16, d2resource.PaletteStatic) @@ -149,6 +159,7 @@ func NewHUD( zoneChangeText: zoneLabel, healthGlobe: healthGlobe, manaGlobe: manaGlobe, + gameControls: gameControls, } hud.Logger = d2util.NewLogger() @@ -177,6 +188,27 @@ func (h *HUD) Load() { h.loadCustomWidgets() h.loadUIButtons() + // nolint:gomnd // dividing by 2 (const) + h.addStatsButton = h.uiManager.NewButton(d2ui.ButtonTypeAddSkill, "") + h.addStatsButton.SetPosition(addStatsButtonX, addStatsButtonY) + h.addStatsButton.SetVisible(false) + bw, bh := h.addStatsButton.GetSize() + statsTooltip := h.uiManager.NewTooltip(d2resource.Font16, d2resource.PaletteSky, d2ui.TooltipXCenter, d2ui.TooltipYTop) + statsTooltip.SetPosition(addStatsButtonX+bw/2, addStatsButtonY-bh/2) + statsTooltip.SetText(h.asset.TranslateString("strlvlup")) + h.addStatsButton.SetTooltip(statsTooltip) + h.panelGroup.AddWidget(h.addStatsButton) + + h.addSkillButton = h.uiManager.NewButton(d2ui.ButtonTypeAddSkill, "") + h.addSkillButton.SetPosition(addSkillButtonX, addSkillButtonY) + h.addSkillButton.SetVisible(false) + bw, bh = h.addSkillButton.GetSize() + skillTooltip := h.uiManager.NewTooltip(d2resource.Font16, d2resource.PaletteSky, d2ui.TooltipXCenter, d2ui.TooltipYTop) + skillTooltip.SetPosition(addSkillButtonX+bw/2, addSkillButtonY-bh/2) + skillTooltip.SetText(h.asset.TranslateString("strnewskl")) + h.addSkillButton.SetTooltip(skillTooltip) + h.panelGroup.AddWidget(h.addSkillButton) + h.panelGroup.SetVisible(true) } @@ -226,7 +258,6 @@ func (h *HUD) loadCustomWidgets() { } func (h *HUD) loadSkillResources() { - // https://github.com/OpenDiablo2/OpenDiablo2/issues/799 genericSkillsSprite, err := h.uiManager.NewSprite(d2resource.GenericSkills, d2resource.PaletteSky) if err != nil { h.Error(err.Error()) @@ -281,21 +312,6 @@ func (h *HUD) loadTooltips() { labelY := staminaExperienceY - halfLabelHeight h.staminaTooltip.SetPosition(labelX, labelY) - // runwalk tooltip - h.runWalkTooltip = h.uiManager.NewTooltip(d2resource.Font16, d2resource.PaletteSky, d2ui.TooltipXCenter, d2ui.TooltipYBottom) - rect = &h.actionableRegions[walkRun].rect - - halfButtonWidth = rect.Width >> 1 - halfButtonHeight := rect.Height >> 1 - - centerX = rect.Left + halfButtonWidth - centerY := rect.Top + halfButtonHeight - - _, labelHeight = h.runWalkTooltip.GetSize() - labelX = centerX - labelY = centerY - halfButtonHeight - labelHeight - h.runWalkTooltip.SetPosition(labelX, labelY) - // experience tooltip h.experienceTooltip = h.uiManager.NewTooltip(d2resource.Font16, d2resource.PaletteSky, d2ui.TooltipXCenter, d2ui.TooltipYTop) rect = &h.actionableRegions[stamina].rect @@ -316,8 +332,21 @@ func (h *HUD) loadUIButtons() { h.runButton = h.uiManager.NewButton(d2ui.ButtonTypeRun, "") h.runButton.SetPosition(runButtonX, runButtonY) h.runButton.OnActivated(func() { h.onToggleRunButton(false) }) - h.runButton.SetTooltip(h.runWalkTooltip) + + h.runWalkTooltip = h.uiManager.NewTooltip(d2resource.Font16, d2resource.PaletteSky, d2ui.TooltipXCenter, d2ui.TooltipYTop) + // we must set text first, and then we're getting its height h.updateRunTooltipText() + + bw, bh := h.runButton.GetSize() + _, lh := h.runWalkTooltip.GetSize() + // nolint:gomnd // dividing by 2 (const) + labelX := runButtonX + bw/2 + // nolint:gomnd // dividing by 2 (const) + labelY := runButtonY - bh/2 - lh/2 + + h.runWalkTooltip.SetPosition(labelX, labelY) + h.runButton.SetTooltip(h.runWalkTooltip) + h.panelGroup.AddWidget(h.runButton) if h.hero.IsRunToggled() { @@ -344,7 +373,6 @@ func (h *HUD) onToggleRunButton(noButton bool) { h.hero.ToggleRunWalk() h.updateRunTooltipText() - // https://github.com/OpenDiablo2/OpenDiablo2/issues/800 h.hero.SetIsRunning(h.hero.IsRunToggled()) } diff --git a/d2game/d2player/inventory.go b/d2game/d2player/inventory.go index 4b0875d3..d28a4c29 100644 --- a/d2game/d2player/inventory.go +++ b/d2game/d2player/inventory.go @@ -104,7 +104,7 @@ func (g *Inventory) Load() { g.panelGroup = g.uiManager.NewWidgetGroup(d2ui.RenderPriorityInventory) - frame := d2ui.NewUIFrame(g.asset, g.uiManager, d2ui.FrameRight) + frame := g.uiManager.NewUIFrame(d2ui.FrameRight) g.panelGroup.AddWidget(frame) g.panel, err = g.uiManager.NewSprite(d2resource.InventoryCharacterPanel, d2resource.PaletteSky) diff --git a/d2game/d2player/key_binding_menu.go b/d2game/d2player/key_binding_menu.go index fa0da7b7..b5c7dd61 100644 --- a/d2game/d2player/key_binding_menu.go +++ b/d2game/d2player/key_binding_menu.go @@ -666,7 +666,7 @@ func (menu *KeyBindingMenu) onDefaultClicked() error { func (menu *KeyBindingMenu) onAcceptClicked() error { for gameEvent, change := range menu.changesToBeSaved { menu.keyMap.SetPrimaryBinding(gameEvent, change.primary) - menu.keyMap.SetSecondaryBinding(gameEvent, change.primary) + menu.keyMap.SetSecondaryBinding(gameEvent, change.secondary) } menu.changesToBeSaved = make(map[d2enum.GameEvent]*bindingChange) diff --git a/d2game/d2player/quest_log.go b/d2game/d2player/quest_log.go index c35d866a..4de2b90f 100644 --- a/d2game/d2player/quest_log.go +++ b/d2game/d2player/quest_log.go @@ -2,7 +2,6 @@ package d2player import ( "fmt" - "image/color" "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" @@ -52,6 +51,8 @@ const ( questTabXOffset = 61 ) +const questCompleteAnimationDuration = 3 + func (s *QuestLog) getPositionForSocket(number int) (x, y int) { pos := []struct { x int @@ -72,6 +73,7 @@ func (s *QuestLog) getPositionForSocket(number int) (x, y int) { func NewQuestLog(asset *d2asset.AssetManager, ui *d2ui.UIManager, l d2util.LogLevel, + audioProvider d2interface.AudioProvider, act int) *QuestLog { originX := 0 originY := 0 @@ -80,12 +82,12 @@ func NewQuestLog(asset *d2asset.AssetManager, qs := map[int]int{ 0: -2, 1: -2, - 2: -1, + 2: -2, 3: 0, 4: 1, - 5: 2, + 5: 4, 6: 3, - 7: 0, + 7: -1, 8: 0, 9: 0, 10: 0, @@ -107,9 +109,9 @@ func NewQuestLog(asset *d2asset.AssetManager, 26: 0, } - var quests [d2enum.ActsNumber]*d2ui.WidgetGroup + var quests [d2enum.ActsNumber]*questEntire for i := 0; i < d2enum.ActsNumber; i++ { - quests[i] = ui.NewWidgetGroup(d2ui.RenderPriorityQuestLog) + quests[i] = &questEntire{WidgetGroup: ui.NewWidgetGroup(d2ui.RenderPriorityQuestLog)} } var tabs [d2enum.ActsNumber]questLogTab @@ -130,6 +132,7 @@ func NewQuestLog(asset *d2asset.AssetManager, quests: quests, questStatus: qs, maxPlayersAct: mpa, + audioProvider: audioProvider, } ql.Logger = d2util.NewLogger() @@ -150,10 +153,12 @@ type QuestLog struct { selectedQuest int act int tab [d2enum.ActsNumber]questLogTab + audioProvider d2interface.AudioProvider + completeSound d2interface.SoundEffect questName *d2ui.Label questDescr *d2ui.Label - quests [d2enum.ActsNumber]*d2ui.WidgetGroup + quests [d2enum.ActsNumber]*questEntire questStatus map[int]int maxPlayersAct int @@ -164,6 +169,13 @@ type QuestLog struct { *d2util.Logger } +type questEntire struct { + *d2ui.WidgetGroup + icons []*d2ui.Sprite + buttons []*d2ui.Button + sockets []*d2ui.Sprite +} + /* questIconTab returns path to quest animation using its act and number. From d2resource: QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6"*/ @@ -177,6 +189,11 @@ const ( notStartedFrame = 26 ) +const ( + socketNormalFrame = 0 + socketHighlightedFrame = 1 +) + const questDescriptionLenght = 30 type questLogTab struct { @@ -190,7 +207,13 @@ func (s *QuestLog) Load() { s.panelGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) - frame := d2ui.NewUIFrame(s.asset, s.uiManager, d2ui.FrameLeft) + // quest completion sound. + s.completeSound, err = s.audioProvider.LoadSound(d2resource.QuestLogDoneSfx, false, false) + if err != nil { + s.Error(err.Error()) + } + + frame := s.uiManager.NewUIFrame(d2ui.FrameLeft) s.panelGroup.AddWidget(frame) s.panel, err = s.uiManager.NewSprite(d2resource.QuestLogBg, d2resource.PaletteSky) @@ -199,7 +222,7 @@ func (s *QuestLog) Load() { } w, h := frame.GetSize() - staticPanel := s.uiManager.NewCustomWidgetCached(s.renderStaticMenu, w, h) + staticPanel := s.uiManager.NewCustomWidgetCached(s.renderStaticPanelFrames, w, h) s.panelGroup.AddWidget(staticPanel) closeButton := s.uiManager.NewButton(d2ui.ButtonTypeSquareClose, "") @@ -216,30 +239,34 @@ func (s *QuestLog) Load() { s.questName = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic) s.questName.Alignment = d2ui.HorizontalAlignCenter - s.questName.Color[0] = rgbaColor(white) + s.questName.Color[0] = d2util.Color(white) s.questName.SetPosition(questNameLabelX, questNameLabelY) s.panelGroup.AddWidget(s.questName) s.questDescr = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic) s.questDescr.Alignment = d2ui.HorizontalAlignLeft - s.questDescr.Color[0] = rgbaColor(white) + s.questDescr.Color[0] = d2util.Color(white) s.questDescr.SetPosition(questDescrLabelX, questDescrLabelY) s.panelGroup.AddWidget(s.questDescr) s.loadTabs() + // creates quest boards for each act for i := 0; i < d2enum.ActsNumber; i++ { - s.quests[i] = s.loadQuestIconsForAct(i + 1) + item, icons, buttons, sockets := s.loadQuestBoard(i + 1) + s.quests[i] = &questEntire{item, icons, buttons, sockets} } s.panelGroup.SetVisible(false) } +// loadTabs loads quest log tabs func (s *QuestLog) loadTabs() { var err error tabsResource := d2resource.WPTabs + // create tabs only for 'discovered' acts for i := 0; i < s.maxPlayersAct; i++ { currentValue := i @@ -248,7 +275,8 @@ func (s *QuestLog) loadTabs() { s.Error(err.Error()) } - // nolint:gomnd // it's constant + // nolint:gomnd // it's constant. + // each tab has two frames (active / inactive) frame := 2 * currentValue err := s.tab[i].sprite.SetCurrentFrame(frame) @@ -266,12 +294,15 @@ func (s *QuestLog) loadTabs() { s.panelGroup.AddWidget(s.tab[i].invisibleButton) } + // sets tab to current player's act. s.setTab(s.act - 1) } -func (s *QuestLog) loadQuestIconsForAct(act int) *d2ui.WidgetGroup { - wg := s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) +// loadQuestBoard creates quest fields (socket, button, icon) for specified act +func (s *QuestLog) loadQuestBoard(act int) (wg *d2ui.WidgetGroup, icons []*d2ui.Sprite, buttons []*d2ui.Button, sockets []*d2ui.Sprite) { + wg = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) + // sets number of quests in act (for act 4 it's only 3, else 6) var questsInAct int if act == d2enum.Act4 { questsInAct = d2enum.HalfQuestsNumber @@ -279,13 +310,8 @@ func (s *QuestLog) loadQuestIconsForAct(act int) *d2ui.WidgetGroup { questsInAct = d2enum.NormalActQuestsNumber } - var sockets []*d2ui.Sprite - - var buttons []*d2ui.Button - - var icon *d2ui.Sprite - for n := 0; n < questsInAct; n++ { + cw := n x, y := s.getPositionForSocket(n) socket, err := s.uiManager.NewSprite(d2resource.QuestLogSocket, d2resource.PaletteSky) @@ -293,56 +319,69 @@ func (s *QuestLog) loadQuestIconsForAct(act int) *d2ui.WidgetGroup { s.Error(err.Error()) } - socket.SetPosition(x+questOffsetX, y+iconOffsetY+2*questOffsetY) + socket.SetPosition(x, y+iconOffsetY+questOffsetY) sockets = append(sockets, socket) - button := s.uiManager.NewButton(d2ui.ButtonTypeBlankQuestBtn, "") - button.SetPosition(x+questOffsetX, y+questOffsetY) - buttons = append(buttons, button) - - icon, err = s.makeQuestIconForAct(act, n) + icon, err := s.makeQuestIconForAct(act, n, x, y) if err != nil { s.Error(err.Error()) } - icon.SetPosition(x+questOffsetX, y+questOffsetY+iconOffsetY) - wg.AddWidget(icon) + icons = append(icons, icon) + + button := s.uiManager.NewButton(d2ui.ButtonTypeBlankQuestBtn, "") + button.SetPosition(x+questOffsetX, y+questOffsetY) + button.SetEnabled(s.questStatus[s.cordsToQuestID(act, cw)] != d2enum.QuestStatusNotStarted) + buttons = append(buttons, button) } for i := 0; i < questsInAct; i++ { currentQuest := i + + // creates callback for quest button buttons[i].OnActivated(func() { var err error + + // set normal (not-highlighted) frame for each quest socket for j := 0; j < questsInAct; j++ { - err = sockets[j].SetCurrentFrame(0) + err = sockets[j].SetCurrentFrame(socketNormalFrame) if err != nil { s.Error(err.Error()) } } - if act-1 == s.selectedTab { - err = sockets[currentQuest].SetCurrentFrame(1) - if err != nil { - s.Error(err.Error()) - } + + // highlights appropriate socket + err = sockets[currentQuest].SetCurrentFrame(socketHighlightedFrame) + if err != nil { + s.Error(err.Error()) } + + // sets quest labels s.onQuestClicked(currentQuest + 1) }) } + // adds sockets to widget group for _, s := range sockets { wg.AddWidget(s) } + // adds buttons to widget group for _, b := range buttons { wg.AddWidget(b) } + // adds icons to widget group + for _, i := range icons { + wg.AddWidget(i) + } + wg.SetVisible(false) - return wg + return wg, icons, buttons, sockets } -func (s *QuestLog) makeQuestIconForAct(act, n int) (*d2ui.Sprite, error) { +func (s *QuestLog) makeQuestIconForAct(act, n, x, y int) (*d2ui.Sprite, error) { iconResource := s.questIconsTable(act, n) icon, err := s.uiManager.NewSprite(iconResource, d2resource.PaletteSky) @@ -354,27 +393,56 @@ func (s *QuestLog) makeQuestIconForAct(act, n int) (*d2ui.Sprite, error) { case d2enum.QuestStatusCompleted: err = icon.SetCurrentFrame(completedFrame) case d2enum.QuestStatusCompleting: - // that's not complet now + // animation will be played after quest-log panel is opened (see s.playQuestAnimation) err = icon.SetCurrentFrame(0) - if err != nil { - s.Error(err.Error()) - } - - icon.PlayForward() - icon.SetPlayLoop(false) - err = icon.SetCurrentFrame(completedFrame) - s.questStatus[s.cordsToQuestID(act, n)] = d2enum.QuestStatusCompleted case d2enum.QuestStatusNotStarted: err = icon.SetCurrentFrame(notStartedFrame) default: err = icon.SetCurrentFrame(inProgresFrame) } + icon.SetPosition(x+questOffsetX, y+questOffsetY+iconOffsetY) + return icon, err } +// playQuestAnimations plays animations for quests (when status=questStatusCompleting) +func (s *QuestLog) playQuestAnimations() { + for j, i := range s.quests[s.selectedTab].icons { + questID := s.cordsToQuestID(s.selectedTab+1, j) + if s.questStatus[questID] == d2enum.QuestStatusCompleting { + s.completeSound.Play() + + // quest should be highlighted and it's label should be displayed + s.quests[s.selectedTab].buttons[j].Activate() + + i.SetPlayLength(questCompleteAnimationDuration) + i.PlayForward() + i.SetPlayLoop(false) + } + } +} + +// stopPlayedAnimation stops currently played animations and sets quests in +// completing state to completed (should be used, when quest log is closing) +func (s *QuestLog) stopPlayedAnimations() { + // stops all played animations + for j, i := range s.quests[s.selectedTab].icons { + questID := s.cordsToQuestID(s.selectedTab+1, j) + if s.questStatus[questID] == d2enum.QuestStatusCompleting { + s.questStatus[questID] = d2enum.QuestStatusCompleted + + err := i.SetCurrentFrame(completedFrame) + if err != nil { + s.Error(err.Error()) + } + } + } +} + +// setQuestLabel loads quest labels text (title and description) func (s *QuestLog) setQuestLabel() { - if s.selectedQuest == 0 { + if s.selectedQuest == d2enum.QuestNone { s.questName.SetText("") s.questDescr.SetText("") @@ -383,9 +451,9 @@ func (s *QuestLog) setQuestLabel() { s.questName.SetText(s.asset.TranslateString(fmt.Sprintf("qstsa%dq%d", s.selectedTab+1, s.selectedQuest))) - status := s.questStatus[s.cordsToQuestID(s.selectedTab+1, s.selectedQuest)] + status := s.questStatus[s.cordsToQuestID(s.selectedTab+1, s.selectedQuest)-1] switch status { - case d2enum.QuestStatusCompleted: + case d2enum.QuestStatusCompleted, d2enum.QuestStatusCompleting: s.questDescr.SetText( strings.Join( d2util.SplitIntoLinesWithMaxWidth( @@ -396,37 +464,61 @@ func (s *QuestLog) setQuestLabel() { case d2enum.QuestStatusNotStarted: s.questDescr.SetText("") default: - s.questDescr.SetText(strings.Join( - d2util.SplitIntoLinesWithMaxWidth( - s.asset.TranslateString( - fmt.Sprintf("qstsa%dq%d%d", s.selectedTab+1, s.selectedQuest, status), - ), - questDescriptionLenght), - "\n"), - ) + str := fmt.Sprintf("qstsa%dq%d%d", s.selectedTab+1, s.selectedQuest, status) + descr := s.asset.TranslateString(str) + + // if description not found + if str == descr { + s.questDescr.SetText("") + } else { + s.questDescr.SetText(strings.Join( + d2util.SplitIntoLinesWithMaxWidth( + descr, questDescriptionLenght), + "\n"), + ) + } + } +} + +// switch all socket (in current tab) to normal state +func (s *QuestLog) clearHighlightment() { + for _, i := range s.quests[s.selectedTab].sockets { + err := i.SetCurrentFrame(socketNormalFrame) + if err != nil { + s.Error(err.Error()) + } } } func (s *QuestLog) setTab(tab int) { var mod int + // before we leafe current tab, we need to switch highlighted + // quest socket to normal frame + s.clearHighlightment() + s.selectedTab = tab s.selectedQuest = d2enum.QuestNone s.setQuestLabel() + s.playQuestAnimations() + // displays appropriate quests board for i := 0; i < s.maxPlayersAct; i++ { s.quests[i].SetVisible(tab == i) } + // "highlights" appropriate tab for i := 0; i < s.maxPlayersAct; i++ { cv := i + // converts bool to 1/0 if cv == s.selectedTab { mod = 0 } else { mod = 1 } + // sets tab sprite to highlighted/non-highlighted err := s.tab[cv].sprite.SetCurrentFrame(2*cv + mod) if err != nil { s.Error(err.Error()) @@ -440,8 +532,9 @@ func (s *QuestLog) onQuestClicked(number int) { s.Infof("Quest number %d in tab %d clicked", number, s.selectedTab) } +// func (s *QuestLog) onDescrClicked() { - // + s.Info("Quest description button clicked") } // IsOpen returns true if the hero status panel is open @@ -463,6 +556,7 @@ func (s *QuestLog) Open() { s.isOpen = true s.panelGroup.SetVisible(true) s.setTab(s.selectedTab) + s.playQuestAnimations() } // Close closed the hero status panel @@ -474,6 +568,8 @@ func (s *QuestLog) Close() { s.quests[i].SetVisible(false) } + s.stopPlayedAnimations() + s.onCloseCb() } @@ -484,11 +580,22 @@ func (s *QuestLog) SetOnCloseCb(cb func()) { // Advance updates labels on the panel func (s *QuestLog) Advance(elapsed float64) { - // -} + if !s.IsOpen() { + return + } -func (s *QuestLog) renderStaticMenu(target d2interface.Surface) { - s.renderStaticPanelFrames(target) + for j, i := range s.quests[s.selectedTab].icons { + questID := s.cordsToQuestID(s.selectedTab+1, j) + if s.questStatus[questID] == d2enum.QuestStatusCompleting { + if err := i.Advance(elapsed); err != nil { + s.Error(err.Error()) + } + + if i.GetCurrentFrame() == completedFrame { + s.questStatus[questID] = d2enum.QuestStatusCompleted + } + } + } } // nolint:dupl // I think it is OK, to duplicate this function @@ -527,32 +634,6 @@ func (s *QuestLog) renderStaticPanelFrames(target d2interface.Surface) { } } -// copy from character select (github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen/character_select.go) -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 -} - func (s *QuestLog) cordsToQuestID(act, number int) int { key := (act-1)*d2enum.NormalActQuestsNumber + number if act > d2enum.Act4 { @@ -561,25 +642,3 @@ func (s *QuestLog) cordsToQuestID(act, number int) int { return key } - -//nolint:varcheck,unused // I think, it will be used, if not, we can just remove it -func (s *QuestLog) questIDToCords(id int) (act, number int) { - act = 1 - - for i := 0; i < d2enum.ActsNumber; i++ { - if id < d2enum.NormalActQuestsNumber { - break - } - - act++ - - id -= d2enum.NormalActQuestsNumber - } - - number = id - if act > d2enum.Act4 { - number -= d2enum.HalfQuestsNumber - } - - return act, number -} diff --git a/d2game/d2player/skilltree.go b/d2game/d2player/skilltree.go index 3f5ddae0..b60c6509 100644 --- a/d2game/d2player/skilltree.go +++ b/d2game/d2player/skilltree.go @@ -3,6 +3,7 @@ package d2player import ( "errors" "fmt" + "strconv" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -56,6 +57,10 @@ const ( frameSelectedTab3Full = 13 ) +const ( + remainingPointsLabelX, remainingPointsLabelY = 677, 128 +) + const ( skillTreePanelX = 401 skillTreePanelY = 64 @@ -87,6 +92,7 @@ type skillTreeHeroTypeResources struct { func newSkillTree( skills map[int]*d2hero.HeroSkill, heroClass d2enum.Hero, + hero *d2hero.HeroStatsState, asset *d2asset.AssetManager, l d2util.LogLevel, ui *d2ui.UIManager, @@ -98,6 +104,7 @@ func newSkillTree( uiManager: ui, originX: skillTreePanelX, originY: skillTreePanelY, + stats: hero, tab: [numTabs]*skillTreeTab{ {}, {}, @@ -114,24 +121,25 @@ func newSkillTree( } type skillTree struct { - resources *skillTreeHeroTypeResources - asset *d2asset.AssetManager - uiManager *d2ui.UIManager - skills map[int]*d2hero.HeroSkill - skillIcons []*skillIcon - heroClass d2enum.Hero - frame *d2ui.UIFrame - availSPLabel *d2ui.Label - closeButton *d2ui.Button - tab [numTabs]*skillTreeTab - isOpen bool - originX int - originY int - selectedTab int - onCloseCb func() - panelGroup *d2ui.WidgetGroup - iconGroup *d2ui.WidgetGroup - panel *d2ui.CustomWidget + resources *skillTreeHeroTypeResources + asset *d2asset.AssetManager + uiManager *d2ui.UIManager + skills map[int]*d2hero.HeroSkill + skillIcons []*skillIcon + heroClass d2enum.Hero + availSPLabel *d2ui.Label + closeButton *d2ui.Button + tab [numTabs]*skillTreeTab + remainingPoints *d2ui.Label + isOpen bool + originX int + originY int + selectedTab int + onCloseCb func() + panelGroup *d2ui.WidgetGroup + iconGroup *d2ui.WidgetGroup + panel *d2ui.CustomWidget + stats *d2hero.HeroStatsState *d2util.Logger l d2util.LogLevel @@ -144,14 +152,20 @@ func (s *skillTree) load() { s.panel = s.uiManager.NewCustomWidget(s.Render, 400, 600) s.panelGroup.AddWidget(s.panel) - s.frame = d2ui.NewUIFrame(s.asset, s.uiManager, d2ui.FrameRight) - s.panelGroup.AddWidget(s.frame) + frame := s.uiManager.NewUIFrame(d2ui.FrameRight) + s.panelGroup.AddWidget(frame) s.closeButton = s.uiManager.NewButton(d2ui.ButtonTypeSquareClose, "") s.closeButton.SetVisible(false) s.closeButton.OnActivated(func() { s.Close() }) s.panelGroup.AddWidget(s.closeButton) + s.remainingPoints = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteSky) + s.remainingPoints.SetPosition(remainingPointsLabelX, remainingPointsLabelY) + s.remainingPoints.Alignment = d2ui.HorizontalAlignCenter + s.remainingPoints.SetText(strconv.Itoa(s.stats.SkillPoints)) + s.panelGroup.AddWidget(s.remainingPoints) + if err := s.setHeroTypeResourcePath(); err != nil { s.Error(err.Error()) } diff --git a/d2networking/d2client/d2localclient/local_client_connection.go b/d2networking/d2client/d2localclient/local_client_connection.go index 391160b7..3923d42b 100644 --- a/d2networking/d2client/d2localclient/local_client_connection.go +++ b/d2networking/d2client/d2localclient/local_client_connection.go @@ -87,19 +87,16 @@ func (l *LocalClientConnection) Open(_, saveFilePath string) error { // Close disconnects from the server and destroys it. func (l *LocalClientConnection) Close() error { - sc, err := d2netpacket.CreateServerClosedPacket() + disconnectRequest, err := d2netpacket.CreatePlayerDisconnectRequestPacket(l.uniqueID) if err != nil { return err } - err = l.SendPacketToServer(sc) + err = l.SendPacketToServer(disconnectRequest) if err != nil { return err } - l.gameServer.OnClientDisconnected(l) - l.gameServer.Stop() - return nil } diff --git a/d2networking/d2client/d2remoteclient/remote_client_connection.go b/d2networking/d2client/d2remoteclient/remote_client_connection.go index 9f47744e..57df11ef 100644 --- a/d2networking/d2client/d2remoteclient/remote_client_connection.go +++ b/d2networking/d2client/d2remoteclient/remote_client_connection.go @@ -3,6 +3,7 @@ package d2remoteclient import ( "encoding/json" "fmt" + "io" "net" "strings" @@ -131,12 +132,10 @@ func (r *RemoteClientConnection) SetClientListener(listener d2networking.ClientL // SendPacketToServer compresses the JSON encoding of a NetPacket and // sends it to the server. func (r *RemoteClientConnection) SendPacketToServer(packet d2netpacket.NetPacket) error { - data, err := json.Marshal(packet) - if err != nil { - return err - } + encoder := json.NewEncoder(r.tcpConnection) - if _, err = r.tcpConnection.Write(data); err != nil { + err := encoder.Encode(packet) + if err != nil { return err } @@ -146,15 +145,21 @@ func (r *RemoteClientConnection) SendPacketToServer(packet d2netpacket.NetPacket // serverListener runs a while loop, reading from the GameServer's TCP // connection. func (r *RemoteClientConnection) serverListener() { - var packet d2netpacket.NetPacket - decoder := json.NewDecoder(r.tcpConnection) for { + var packet d2netpacket.NetPacket + err := decoder.Decode(&packet) if err != nil { - r.Errorf("failed to decode the packet, err: %v\n", err) - return + switch err { + case io.EOF: + break // the other side closed the connection + default: + r.Errorf("failed to decode the packet, err: %v\n", err) + } + + return // allow the connection to close } p, err := r.decodeToPacket(packet.PacketType, string(packet.PacketData)) @@ -186,102 +191,29 @@ func (r *RemoteClientConnection) bytesToJSON(buffer []byte) (string, d2netpacket func (r *RemoteClientConnection) decodeToPacket( t d2netpackettype.NetPacketType, data string) (d2netpacket.NetPacket, error) { - var np = d2netpacket.NetPacket{} - - var err error + var ( + np = d2netpacket.NetPacket{} + err error + p interface{} + ) switch t { case d2netpackettype.GenerateMap: - var p d2netpacket.GenerateMapPacket - if err = json.Unmarshal([]byte(data), &p); err != nil { - break - } - - mp, marshalErr := d2netpacket.MarshalPacket(p) - if marshalErr != nil { - r.Errorf("MarshalPacket: %v", marshalErr) - } - - np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} - + p, err = d2netpacket.UnmarshalGenerateMap([]byte(data)) case d2netpackettype.MovePlayer: - var p d2netpacket.MovePlayerPacket - if err = json.Unmarshal([]byte(data), &p); err != nil { - break - } - - mp, marshalErr := d2netpacket.MarshalPacket(p) - if marshalErr != nil { - r.Errorf("MarshalPacket: %v", marshalErr) - } - - np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} - + p, err = d2netpacket.UnmarshalMovePlayer([]byte(data)) case d2netpackettype.UpdateServerInfo: - var p d2netpacket.UpdateServerInfoPacket - if err = json.Unmarshal([]byte(data), &p); err != nil { - break - } - - mp, marshalErr := d2netpacket.MarshalPacket(p) - if marshalErr != nil { - r.Errorf("MarshalPacket: %v", marshalErr) - } - - np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} - + p, err = d2netpacket.UnmarshalUpdateServerInfo([]byte(data)) case d2netpackettype.AddPlayer: - var p d2netpacket.AddPlayerPacket - if err = json.Unmarshal([]byte(data), &p); err != nil { - break - } - - mp, marshalErr := d2netpacket.MarshalPacket(p) - if marshalErr != nil { - r.Errorf("MarshalPacket: %v", marshalErr) - } - - np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} - + p, err = d2netpacket.UnmarshalAddPlayer([]byte(data)) case d2netpackettype.CastSkill: - var p d2netpacket.CastPacket - if err = json.Unmarshal([]byte(data), &p); err != nil { - break - } - - mp, marshalErr := d2netpacket.MarshalPacket(p) - if marshalErr != nil { - r.Errorf("MarshalPacket: %v", marshalErr) - } - - np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} - + p, err = d2netpacket.UnmarshalCast([]byte(data)) case d2netpackettype.Ping: - var p d2netpacket.PingPacket - if err = json.Unmarshal([]byte(data), &p); err != nil { - break - } - - mp, marshalErr := d2netpacket.MarshalPacket(p) - if marshalErr != nil { - r.Errorf("MarshalPacket: %v", marshalErr) - } - - np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} - + p, err = d2netpacket.UnmarshalPing([]byte(data)) case d2netpackettype.PlayerDisconnectionNotification: - var p d2netpacket.PlayerDisconnectRequestPacket - if err = json.Unmarshal([]byte(data), &p); err != nil { - break - } - - mp, marshalErr := d2netpacket.MarshalPacket(p) - if marshalErr != nil { - r.Errorf("MarshalPacket: %v", marshalErr) - } - - np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} - + p, err = d2netpacket.UnmarshalPlayerDisconnectionRequest([]byte(data)) + case d2netpackettype.ServerClosed: + p, err = d2netpacket.UnmarshalServerClosed([]byte(data)) default: err = fmt.Errorf("RemoteClientConnection: unrecognized packet type: %v", t) } @@ -290,5 +222,12 @@ func (r *RemoteClientConnection) decodeToPacket( return np, err } + mp, marshalErr := d2netpacket.MarshalPacket(p) + if marshalErr != nil { + r.Errorf("MarshalPacket: %v", marshalErr) + } + + np = d2netpacket.NetPacket{PacketType: t, PacketData: mp} + return np, nil } diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index d3b197aa..0737223d 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -160,8 +160,9 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { g.Errorf("GameClient: error responding to server ping: %s", err) } case d2netpackettype.PlayerDisconnectionNotification: - // Not implemented - g.Infof("RemoteClientConnection: received disconnect: %s", packet.PacketData) + if err := g.handlePlayerDisconnectionPacket(packet); err != nil { + return err + } case d2netpackettype.ServerClosed: // https://github.com/OpenDiablo2/OpenDiablo2/issues/802 g.Infof("Server has been closed") @@ -446,6 +447,19 @@ func (g *GameClient) handlePingPacket() error { return nil } +func (g *GameClient) handlePlayerDisconnectionPacket(packet d2netpacket.NetPacket) error { + disconnectPacket, err := d2netpacket.UnmarshalPlayerDisconnectionRequest(packet.PacketData) + if err != nil { + return err + } + + player := g.Players[disconnectPacket.ID] + g.MapEngine.RemoveEntity(player) + delete(g.Players, disconnectPacket.ID) + + return nil +} + // IsSinglePlayer returns a bool for whether the game is a single-player game func (g *GameClient) IsSinglePlayer() bool { return g.connectionType == d2clientconnectiontype.Local diff --git a/d2networking/d2netpacket/packet_add_player.go b/d2networking/d2netpacket/packet_add_player.go index a503a075..795879a6 100644 --- a/d2networking/d2netpacket/packet_add_player.go +++ b/d2networking/d2netpacket/packet_add_player.go @@ -35,8 +35,7 @@ func CreateAddPlayerPacket( stats *d2hero.HeroStatsState, skills map[int]*d2hero.HeroSkill, equipment d2inventory.CharacterEquipment, - leftSkill, rightSkill int, - gold int) (NetPacket, error) { + leftSkill, rightSkill, gold int) (NetPacket, error) { addPlayerPacket := AddPlayerPacket{ ID: id, Name: name, diff --git a/d2networking/d2netpacket/packet_ping.go b/d2networking/d2netpacket/packet_ping.go index edad91f0..5ce7b61a 100644 --- a/d2networking/d2netpacket/packet_ping.go +++ b/d2networking/d2netpacket/packet_ping.go @@ -1,4 +1,4 @@ -package d2netpacket +package d2netpacket //nolint:dupl // ServerClosed and Ping just happen to be very similar packets import ( "encoding/json" @@ -30,3 +30,13 @@ func CreatePingPacket() (NetPacket, error) { PacketData: b, }, nil } + +// UnmarshalPing unmarshals the given data to a PingPacket struct +func UnmarshalPing(packet []byte) (PingPacket, error) { + var p PingPacket + if err := json.Unmarshal(packet, &p); err != nil { + return p, err + } + + return p, nil +} diff --git a/d2networking/d2netpacket/packet_server_closed.go b/d2networking/d2netpacket/packet_server_closed.go index eb7fcb19..79c05833 100644 --- a/d2networking/d2netpacket/packet_server_closed.go +++ b/d2networking/d2netpacket/packet_server_closed.go @@ -1,4 +1,4 @@ -package d2netpacket +package d2netpacket //nolint:dupl // ServerClosed and Ping just happen to be very similar packets import ( "encoding/json" diff --git a/d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go b/d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go index 36bf953c..7a457feb 100644 --- a/d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go +++ b/d2networking/d2server/d2tcpclientconnection/tcp_client_connection.go @@ -33,12 +33,9 @@ func (t TCPClientConnection) GetUniqueID() string { // SendPacketToClient marshals and sends (writes) NetPackets func (t *TCPClientConnection) SendPacketToClient(p d2netpacket.NetPacket) error { - packet, err := json.Marshal(p) - if err != nil { - return err - } + encoder := json.NewEncoder(t.tcpConnection) - _, err = t.tcpConnection.Write(packet) + err := encoder.Encode(p) if err != nil { return err } diff --git a/d2networking/d2server/game_server.go b/d2networking/d2server/game_server.go index de20b456..8e349281 100644 --- a/d2networking/d2server/game_server.go +++ b/d2networking/d2server/game_server.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "io" "net" "sync" "time" @@ -16,6 +17,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2tcpclientconnection" @@ -50,12 +52,19 @@ type GameServer struct { scriptEngine *d2script.ScriptEngine seed int64 maxConnections int - packetManagerChan chan []byte + packetManagerChan chan ReceivedPacket heroStateFactory *d2hero.HeroStateFactory *d2util.Logger } +// ReceivedPacket encapsulates the data necessary for the packet manager goroutine to process data from clients. +// The packet manager needs to know who sent the data, in addition to the data itself. +type ReceivedPacket struct { + Client ClientConnection + Packet d2netpacket.NetPacket +} + // NewGameServer builds a new GameServer that can be started // // ctx: required context item @@ -84,7 +93,7 @@ func NewGameServer(asset *d2asset.AssetManager, connections: make(map[string]ClientConnection), networkServer: networkServer, maxConnections: maxConnections[0], - packetManagerChan: make(chan []byte), + packetManagerChan: make(chan ReceivedPacket), mapEngines: make([]*d2mapengine.MapEngine, 0), scriptEngine: d2script.CreateScriptEngine(), seed: time.Now().UnixNano(), @@ -142,7 +151,13 @@ func (g *GameServer) Start() error { for { c, err := g.listener.Accept() if err != nil { - g.Errorf("Unable to accept connection: %s", err) + select { + case <-g.ctx.Done(): + // this error was just a result of the server closing, don't worry about it + default: + g.Errorf("Unable to accept connection: %s", err) + } + return } @@ -157,6 +172,7 @@ func (g *GameServer) Start() error { func (g *GameServer) Stop() { g.Lock() g.cancel() + g.connections = make(map[string]ClientConnection) if err := g.listener.Close(); err != nil { g.Errorf("failed to close the listener %s, err: %v\n", g.listener.Addr(), err) @@ -173,45 +189,9 @@ func (g *GameServer) packetManager() { case <-g.ctx.Done(): return case p := <-g.packetManagerChan: - ipt, err := d2netpacket.InspectPacketType(p) + err := g.OnPacketReceived(p.Client, p.Packet) if err != nil { - g.Errorf("InspectPacketType: %v", err) - } - - switch ipt { - case d2netpackettype.PlayerConnectionRequest: - player, err := d2netpacket.UnmarshalNetPacket(p) - if err != nil { - g.Errorf("Unable to unmarshal PlayerConnectionRequestPacket: %s\n", err) - } - - g.sendPacketToClients(player) - case d2netpackettype.MovePlayer: - move, err := d2netpacket.UnmarshalNetPacket(p) - if err != nil { - g.Error(err.Error()) - continue - } - - g.sendPacketToClients(move) - case d2netpackettype.CastSkill: - castSkill, err := d2netpacket.UnmarshalNetPacket(p) - if err != nil { - g.Error(err.Error()) - continue - } - - g.sendPacketToClients(castSkill) - case d2netpackettype.SpawnItem: - item, err := d2netpacket.UnmarshalNetPacket(p) - if err != nil { - g.Error(err.Error()) - continue - } - - g.sendPacketToClients(item) - case d2netpackettype.ServerClosed: - g.Stop() + g.Errorf("failed to handle packet received from client %s: %v", p.Client.GetUniqueID(), err) } } } @@ -228,9 +208,10 @@ func (g *GameServer) sendPacketToClients(packet d2netpacket.NetPacket) { // handleConnection accepts an individual connection and starts pooling for new packets. It is recommended this is called // via Go Routine. Context should be a property of the GameServer Struct. func (g *GameServer) handleConnection(conn net.Conn) { - var connected int - - var packet d2netpacket.NetPacket + var ( + connected int + client ClientConnection + ) g.Infof("Accepting connection: %s\n", conn.RemoteAddr().String()) @@ -243,10 +224,18 @@ func (g *GameServer) handleConnection(conn net.Conn) { decoder := json.NewDecoder(conn) for { + var packet d2netpacket.NetPacket + err := decoder.Decode(&packet) if err != nil { - g.Error(err.Error()) - return // exit this connection as we could not read the first packet + switch err { + case io.EOF: + break // the other side closed the connection + default: + g.Error(err.Error()) + } + + return // allow the connection to close } // If this is the first packet we are seeing from this specific connection we first need to see if the client @@ -257,25 +246,7 @@ func (g *GameServer) handleConnection(conn net.Conn) { g.Infof("Closing connection with %s: did not receive new player connection request...", conn.RemoteAddr().String()) } - if err := g.registerConnection(packet.PacketData, conn); err != nil { - switch err { - case errServerFull: // Server is currently full and not accepting new connections. - sf, serverFullErr := d2netpacket.CreateServerFullPacket() - if serverFullErr != nil { - g.Errorf("ServerFullPacket: %v", serverFullErr) - } - - msf, marshalServerFullErr := d2netpacket.MarshalPacket(sf) - if marshalServerFullErr != nil { - g.Errorf("MarshalPacket: %v", marshalServerFullErr) - } - - _, errServerFullPacket := conn.Write(msf) - g.Warningf("%v", errServerFullPacket) - case errPlayerAlreadyExists: // Player is already registered and did not disconnection correctly. - g.Errorf("%v", err) - } - + if client, err = g.registerConnection(packet.PacketData, conn); err != nil { return } @@ -286,7 +257,10 @@ func (g *GameServer) handleConnection(conn net.Conn) { case <-g.ctx.Done(): return default: - g.packetManagerChan <- packet.PacketData + g.packetManagerChan <- ReceivedPacket{ + Client: client, + Packet: packet, + } } } } @@ -296,12 +270,28 @@ func (g *GameServer) handleConnection(conn net.Conn) { // Errors: // - errServerFull // - errPlayerAlreadyExists -func (g *GameServer) registerConnection(b []byte, conn net.Conn) error { +func (g *GameServer) registerConnection(b []byte, conn net.Conn) (ClientConnection, error) { + var client ClientConnection + g.Lock() + defer g.Unlock() // check to see if the server is full if len(g.connections) >= g.maxConnections { - return errServerFull + sf, serverFullErr := d2netpacket.CreateServerFullPacket() + if serverFullErr != nil { + g.Errorf("ServerFullPacket: %v", serverFullErr) + } + + msf, marshalServerFullErr := d2netpacket.MarshalPacket(sf) + if marshalServerFullErr != nil { + g.Errorf("MarshalPacket: %v", marshalServerFullErr) + } + + _, errServerFullPacket := conn.Write(msf) + g.Warningf("%v", errServerFullPacket) + + return client, errServerFull } // if it is not full, unmarshal the playerConnectionRequest @@ -312,29 +302,17 @@ func (g *GameServer) registerConnection(b []byte, conn net.Conn) error { // check to see if the player is already registered if _, ok := g.connections[packet.ID]; ok { - return errPlayerAlreadyExists + g.Errorf("%v", errPlayerAlreadyExists) + return client, errPlayerAlreadyExists } // Client a new TCP Client Connection and add it to the connections map - client := d2tcpclientconnection.CreateTCPClientConnection(conn, packet.ID) + client = d2tcpclientconnection.CreateTCPClientConnection(conn, packet.ID) client.SetPlayerState(packet.PlayerState) - g.Infof("Client connected with an id of %s", client.GetUniqueID()) - g.connections[client.GetUniqueID()] = client - // Temporary position hack -------------------------------------------- - // https://github.com/OpenDiablo2/OpenDiablo2/issues/829 - sx, sy := g.mapEngines[0].GetStartPosition() - clientPlayerState := client.GetPlayerState() - clientPlayerState.X = sx - clientPlayerState.Y = sy - // --------- + g.OnClientConnected(client) - // This really should be deferred however to much time will be spend holding a lock when we attempt to send a packet - g.Unlock() - - g.handleClientConnection(client, sx, sy) - - return nil + return client, nil } // OnClientConnected initializes the given ClientConnection. It sends the @@ -447,12 +425,27 @@ func (g *GameServer) handleClientConnection(client ClientConnection, x, y float6 // OnClientDisconnected removes the given client from the list // of client connections. +// If this client was the host, disconnects all clients and kills GameServer. func (g *GameServer) OnClientDisconnected(client ClientConnection) { g.Infof("Client disconnected with an id of %s", client.GetUniqueID()) delete(g.connections, client.GetUniqueID()) + + if client.GetConnectionType() == d2clientconnectiontype.Local { + g.Info("Host disconnected, game server shuting down") + + serverClosed, err := d2netpacket.CreateServerClosedPacket() + if err != nil { + g.Errorf("failed to generate ServerClosed packet after host disconnected: %s", err) + } else { + g.sendPacketToClients(serverClosed) + } + + g.Stop() + } } -// OnPacketReceived is called by the local client to 'send' a packet to the server. +// OnPacketReceived is called when a packet has been received from a remote client, +// and by the local client to 'send' a packet to the server, // nolint:gocyclo // switch statement on packet type makes sense, no need to change func (g *GameServer) OnPacketReceived(client ClientConnection, packet d2netpacket.NetPacket) error { if g == nil { @@ -490,8 +483,13 @@ func (g *GameServer) OnPacketReceived(client ClientConnection, packet d2netpacke if err != nil { g.Errorf("GameServer: error saving saving Player: %s", err) } + case d2netpackettype.PlayerConnectionRequest: + break // prevent log message. these are handled by handleConnection + case d2netpackettype.PlayerDisconnectionNotification: + g.sendPacketToClients(packet) + g.OnClientDisconnected(client) default: - g.Warningf("GameServer: received unknown packet %T", packet) + g.Warningf("GameServer: received unknown packet %s", packet.PacketType) } return nil diff --git a/docs/game_panels.png b/docs/game_panels.png new file mode 100644 index 00000000..15bf933d Binary files /dev/null and b/docs/game_panels.png differ diff --git a/go.mod b/go.mod index 3b9a57d1..27504538 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,14 @@ require ( github.com/google/uuid v1.1.2 github.com/gravestench/akara v0.0.0-20201208183338-ab0934060133 github.com/gravestench/pho v0.0.0-20201029002250-f9afbd637e4d - github.com/hajimehoshi/ebiten/v2 v2.0.1 + github.com/hajimehoshi/ebiten/v2 v2.0.2 + github.com/pkg/errors v0.9.1 // indirect github.com/pkg/profile v1.5.0 github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac github.com/stretchr/testify v1.4.0 + golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7 // indirect golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 + golang.org/x/sys v0.0.0-20201028215240-c5abc1b1d397 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/sourcemap.v1 v1.0.5 // indirect ) diff --git a/go.sum b/go.sum index 480b63b3..7b52f021 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0 h1:tDnuU0igiBiQFjsvq1Bi7DpoUjqI76VVvW045vpeFeM= github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0/go.mod h1:h/5OEGj4G+fpYxluLjSMZbFY011ZxAntO98nCl8mrCs= @@ -7,6 +8,7 @@ github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4 h1:EBTWhcAX7rNQ80 github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2 h1:Ac1OEHHkbAZ6EUnJahF0GKcU0FjPc/V8F1DvjhKngFE= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= @@ -30,6 +32,8 @@ github.com/gravestench/pho v0.0.0-20201029002250-f9afbd637e4d/go.mod h1:yi5GHMLL github.com/hajimehoshi/bitmapfont/v2 v2.1.0/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= github.com/hajimehoshi/ebiten/v2 v2.0.1 h1:94ucoKKoqiJOZxDod8gdMrroCDy0CO6Ct+Nc9kjsW98= github.com/hajimehoshi/ebiten/v2 v2.0.1/go.mod h1:AbHP/SS226aFTex/izULVwW0D2AuGyqC4AVwilmRjOg= +github.com/hajimehoshi/ebiten/v2 v2.0.2 h1:t8HXO9hJfKlS9tNhht8Ov6xecag0gRl7AkfKgC9hcLE= +github.com/hajimehoshi/ebiten/v2 v2.0.2/go.mod h1:AbHP/SS226aFTex/izULVwW0D2AuGyqC4AVwilmRjOg= github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= @@ -46,6 +50,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -63,6 +69,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -70,12 +77,14 @@ golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckH golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20200801112145-973feb4309de h1:OVJ6QQUBAesB8CZijKDSsXX7xYVtUhrkY0gwMfbi4p4= golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -86,13 +95,16 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201028215240-c5abc1b1d397/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20201009162240-fcf82128ed91/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/utils/extract-mpq/extract-mpq.go b/utils/extract-mpq/extract-mpq.go index 6c556b77..2d4d42c0 100644 --- a/utils/extract-mpq/extract-mpq.go +++ b/utils/extract-mpq/extract-mpq.go @@ -33,13 +33,13 @@ func main() { } filename := flag.Arg(0) - mpq, err := d2mpq.Load(filename) + mpq, err := d2mpq.FromFile(filename) if err != nil { log.Fatal(err) } - list, err := mpq.GetFileList() + list, err := mpq.Listfile() if err != nil { log.Fatal(err) }