Cleaned up d2term

This commit is contained in:
Intyre 2020-12-21 21:46:58 +01:00
parent 8a55e1bd4b
commit 04ec879035
12 changed files with 673 additions and 506 deletions

View File

@ -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 {

View File

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

View File

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

View File

@ -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
}

View File

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

33
d2core/d2term/commmand.go Normal file
View 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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() })
}