mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-20 23:47:16 -05:00
commit
0f658d5dec
132
d2app/app.go
132
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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
33
d2core/d2term/commmand.go
Normal file
33
d2core/d2term/commmand.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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,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
|
||||
@ -68,7 +67,7 @@ type terminal struct {
|
||||
commandIndex int
|
||||
|
||||
lineCount int
|
||||
visState termVis
|
||||
visState visibility
|
||||
visAnim float64
|
||||
|
||||
bgColor color.RGBA
|
||||
@ -77,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
|
||||
}
|
||||
|
||||
@ -139,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
|
||||
}
|
||||
@ -156,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 {
|
||||
@ -181,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
|
||||
}
|
||||
|
||||
@ -211,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)
|
||||
@ -231,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()
|
||||
@ -251,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()
|
||||
|
||||
@ -260,174 +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
|
||||
|
||||
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 +457,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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
82
d2core/d2term/terminal_test.go
Normal file
82
d2core/d2term/terminal_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package d2player
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -966,59 +967,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"}
|
||||
@ -1034,9 +1111,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
|
||||
@ -1072,80 +1149,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() })
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user