From 04ec879035616aa28cffd0c59c034cd415e0fe28 Mon Sep 17 00:00:00 2001 From: Intyre Date: Mon, 21 Dec 2020 21:46:58 +0100 Subject: [PATCH 1/3] Cleaned up d2term --- d2app/app.go | 132 +++++---- d2common/d2interface/terminal.go | 18 +- d2core/d2asset/asset_manager.go | 70 +++-- d2core/d2audio/sound_engine.go | 83 ++++-- d2core/d2map/d2maprenderer/renderer.go | 42 ++- d2core/d2term/commmand.go | 33 +++ d2core/d2term/d2term.go | 4 +- d2core/d2term/terminal.go | 372 ++++++++++--------------- d2core/d2term/terminal_logger.go | 10 +- d2core/d2term/terminal_test.go | 71 +++++ d2game/d2gamescreen/game.go | 127 +++++---- d2game/d2player/game_controls.go | 217 ++++++++------- 12 files changed, 673 insertions(+), 506 deletions(-) create mode 100644 d2core/d2term/commmand.go create mode 100644 d2core/d2term/terminal_test.go diff --git a/d2app/app.go b/d2app/app.go index fcb743b5..afdf3ced 100644 --- a/d2app/app.go +++ b/d2app/app.go @@ -91,12 +91,6 @@ type Options struct { LogLevel *d2util.LogLevel } -type bindTerminalEntry struct { - name string - description string - action interface{} -} - const ( bytesToMegabyte = 1024 * 1024 nSamplesTAlloc = 100 @@ -184,11 +178,6 @@ 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.Options.LogLevel, audio) @@ -351,25 +340,28 @@ 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()) } } @@ -644,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()) @@ -663,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.Infof("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 } @@ -769,42 +769,56 @@ func (a *App) convertFramesToGif() error { 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() { +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 { 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/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/d2audio/sound_engine.go b/d2core/d2audio/sound_engine.go index ee907af0..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" @@ -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/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/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 be345220..4e30e478 100644 --- a/d2core/d2term/terminal.go +++ b/d2core/d2term/terminal.go @@ -6,9 +6,6 @@ import ( "image/color" "log" "math" - "reflect" - "sort" - "strconv" "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" @@ -18,13 +15,13 @@ import ( ) 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 ( @@ -35,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 ( @@ -49,18 +46,22 @@ 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 @@ -68,7 +69,7 @@ type terminal struct { commandIndex int lineCount int - visState termVis + visState visibility visAnim float64 bgColor color.RGBA @@ -77,36 +78,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 } @@ -139,7 +192,7 @@ func (t *terminal) OnKeyDown(event d2interface.KeyEvent) bool { return true } -func (t *terminal) processCommand() { +func (t *Terminal) processCommand() { if t.command == "" { return } @@ -156,17 +209,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 { @@ -181,21 +234,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 } @@ -211,14 +257,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) @@ -231,19 +278,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() @@ -251,7 +298,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() @@ -260,174 +307,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 - - for i := 0; i < actionType.NumIn(); i++ { - actionParam := actionParams[i] - - 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") - } - } - - return paramValues, nil -} - -func (t *terminal) OutputRaw(text string, category d2enum.TermCategory) { - lines := d2util.SplitIntoLinesWithMaxWidth(text, termColCountMax) +// 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 _, line := range lines { // removes color token (this token ends with [0m ) l := strings.Split(line, "\033[0m") line = l[len(l)-1] - t.outputHistory = append(t.outputHistory, termHistoryEntry{line, category}) + t.outputHistory = append(t.outputHistory, historyEntry{line, category}) } } -func (t *terminal) Outputf(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryNone) +// Printf writes a message to the terminal +func (t *Terminal) Printf(format string, params ...interface{}) { + t.Rawf(d2enum.TermCategoryNone, format, params...) } -func (t *terminal) OutputInfof(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryInfo) +// 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) OutputWarningf(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryWarning) +// 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) OutputErrorf(format string, params ...interface{}) { - t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryError) +// 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) 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 { @@ -481,45 +459,3 @@ func parseCommand(command string) []string { return params } - -func createTerminal() (*terminal, error) { - terminal := &terminal{ - lineCount: termRowCount, - bgColor: d2util.Color(darkGrey), - fgColor: d2util.Color(lightGrey), - infoColor: d2util.Color(lightBlue), - warningColor: d2util.Color(yellow), - errorColor: d2util.Color(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..1bfbda22 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.Errorf(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..13b46778 --- /dev/null +++ b/d2core/d2term/terminal_test.go @@ -0,0 +1,71 @@ +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) + } + + term.Execute("clear") + term.Execute("ls") + + 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) + } + + term.Unbind("clear") + term.Clear() + term.Execute("ls") + + lenOutput := len(term.outputHistory) + + const expected = 2 + if lenOutput != expected { + t.Fatalf("got %d expected %d", lenOutput, expected) + } +} diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 2b339a01..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 @@ -395,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/d2player/game_controls.go b/d2game/d2player/game_controls.go index 01fc1cd3..1839b427 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -2,6 +2,7 @@ package d2player import ( "fmt" + "strconv" "strings" "time" @@ -936,59 +937,137 @@ 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) { +// 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.Errorf("invalid argument") + return nil + } + skillRecord := g.asset.Records.Skill.Details[id] skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) if err != nil { - term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) - return + term.Errorf("cannot create skill with ID of %d, error: %s", id, err) + 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) { +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.Errorf("invalid argument") + return nil + } + skillRecord := g.asset.Records.Skill.Details[id] skill, err := g.heroState.CreateHeroSkill(0, skillRecord.Skill) if err != nil { - term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) - return + term.Errorf("cannot create skill with ID of %d, error: %s", id, err) + 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) { + skillRecord := g.asset.Records.Skill.Details[id] + if skillRecord == nil { + term.Errorf("cannot find a skill record for ID: %d", id) + return nil + } + + skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) + if skill == nil { + term.Errorf("cannot create skill: %s", skillRecord.Skill) + return nil + } + + g.hero.Skills[skill.ID] = skill + + if err != nil { + term.Errorf("cannot learn skill for class, error: %s", err) + return nil + } + + g.hud.skillSelectMenu.RegenerateImageCache() + g.Infof("Learned skill: " + skill.Skill) + + return 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"} @@ -1004,9 +1083,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 @@ -1042,80 +1121,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 - } - } - - 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 + term.Errorf("cannot learn skill for class, error: %s", err) + return nil } - 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 nil } - - 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 -} - -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() }) } From 74a006c252b8daef115044c836f788b12a2e23f3 Mon Sep 17 00:00:00 2001 From: Intyre Date: Mon, 21 Dec 2020 22:00:07 +0100 Subject: [PATCH 2/3] A wild } appeared --- d2core/d2term/terminal.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/d2core/d2term/terminal.go b/d2core/d2term/terminal.go index 4e30e478..465905a0 100644 --- a/d2core/d2term/terminal.go +++ b/d2core/d2term/terminal.go @@ -57,8 +57,6 @@ type commandEntry struct { fn func([]string) error } -} - // Terminal handles the in-game terminal type Terminal struct { outputHistory []historyEntry From 570ec238ff248afc8259ff5bebe65d2821a7f7a6 Mon Sep 17 00:00:00 2001 From: Intyre Date: Mon, 21 Dec 2020 22:22:27 +0100 Subject: [PATCH 3/3] Fixed linter issues --- d2core/d2term/terminal_test.go | 19 +++++++++++--- d2game/d2player/game_controls.go | 44 +++++++++++++++----------------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/d2core/d2term/terminal_test.go b/d2core/d2term/terminal_test.go index 13b46778..a2332f01 100644 --- a/d2core/d2term/terminal_test.go +++ b/d2core/d2term/terminal_test.go @@ -18,8 +18,13 @@ func TestTerminal(t *testing.T) { t.Fatalf("got %d expected %d", lenOutput, expected1) } - term.Execute("clear") - term.Execute("ls") + if err := term.Execute("clear"); err != nil { + t.Fatal(err) + } + + if err := term.Execute("ls"); err != nil { + t.Fatal(err) + } lenOutput = len(term.outputHistory) @@ -58,9 +63,15 @@ func TestUnbind(t *testing.T) { t.Fatal(err) } - term.Unbind("clear") + if err := term.Unbind("clear"); err != nil { + t.Fatal(err) + } + term.Clear() - term.Execute("ls") + + if err := term.Execute("ls"); err != nil { + t.Fatal(err) + } lenOutput := len(term.outputHistory) diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index 1839b427..5c2216d7 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -990,11 +990,9 @@ func (g *GameControls) commandSetLeftSkill(term d2interface.Terminal) func(args return nil } - skillRecord := g.asset.Records.Skill.Details[id] - skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) - + skill, err := g.heroSkillByID(id) if err != nil { - term.Errorf("cannot create skill with ID of %d, error: %s", id, err) + term.Errorf(err.Error()) return nil } @@ -1012,11 +1010,9 @@ func (g *GameControls) commandSetRightSkill(term d2interface.Terminal) func(args return nil } - skillRecord := g.asset.Records.Skill.Details[id] - skill, err := g.heroState.CreateHeroSkill(0, skillRecord.Skill) - + skill, err := g.heroSkillByID(id) if err != nil { - term.Errorf("cannot create skill with ID of %d, error: %s", id, err) + term.Errorf(err.Error()) return nil } @@ -1034,25 +1030,13 @@ func (g *GameControls) commandLearnSkillID(term d2interface.Terminal) func(args return nil } - skillRecord := g.asset.Records.Skill.Details[id] - if skillRecord == nil { - term.Errorf("cannot find a skill record for ID: %d", id) - return nil - } - - skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) - if skill == nil { - term.Errorf("cannot create skill: %s", skillRecord.Skill) + skill, err := g.heroSkillByID(id) + if err != nil { + term.Errorf(err.Error()) return nil } g.hero.Skills[skill.ID] = skill - - if err != nil { - term.Errorf("cannot learn skill for class, error: %s", err) - return nil - } - g.hud.skillSelectMenu.RegenerateImageCache() g.Infof("Learned skill: " + skill.Skill) @@ -1060,6 +1044,20 @@ func (g *GameControls) commandLearnSkillID(term d2interface.Terminal) func(args } } +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