mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-10 10:36:42 -05:00
* quest log disabled icon
* removed unused functions for text colorizing
* terminal printing
* fixed saving key change bug
* help overlay text
* removed unused issue comments
* Update Ebiten to v2.0.2
* changed terminal color separator & changed logLevelNone to logLevelDefault in app.go
* status and screens update
* quest animation initial.
* escape menu hotkeys
* hero save file
* add-buttons init
* add-buttons actions
* stats changing: hero stats panel
* skill tre - remaining points label
* revert:hero save file (app.go)
* escape menu hotkeys
* hero save file
* updated d2hero.HeroStatsState
* corrected grammar errors
* animation is played and last frame is completedFrame
* animation stops playing, when quest log is closed & quest socket gets highlighted, when animation is playing & fixed highlight bug
* fixed quest descr bug & added code description
* level-up buttons tooltips
* Replaced kingping with flag package
* Cleanup d2records logging
* Renamed CharStatRecord
* Renamed SoundDetailRecord
* Renamed MonStatRecord
* Renamed ObjectDetailRecord
* Renamed MonStat2Record
* fixed onHover bug in d2ui.Sprite (#992)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* added static checks to d2ui (#990)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* fixed onHover bug in d2ui.Label (#991)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* Revert "fixed onHover bug in d2ui.Sprite"
This reverts commit 8b557062fb
.
* tip-labels in tcpip menu
* skill select menu dependencies (when we open skillselect menu, other panels are closed)
* Cleaned up d2term
* A wild } appeared
* Fixed linter issues
* ckecked value of italian modifier
* game-controls refactor
* fixed build and lint errors
* removed unnecessary switch-case statments from onKeyUp and onEscKey
* fixed bug with terminal's logLevel
* Refactor StreamWriter
* Refactor StreamReader
* Fixed gocritic linter issues
* Reduce GetTiles slice allocation
* Networking bugfixes and cleanup
Make sure connections close properly, without weird error messages
Remove player map entity when a player disconnects from a multiplayer game
Close server properly when host disconnects, handle ServerClose on remote clients
Don't mix JSON decoders and raw TCP writes
Actually handle incoming packets from remote clients
General code cleanup for simplicity and consistency
* Switched to self hosted build agent
* Removed PR requirement for action
* remove build job requirement
* Fiddling with actions names
* Updated workflows
* Fix yaml errors
* Yet another yaml fix
* Removed improper ebiten dependency in d2interface.
* Add checkboxes, checkbox test scene
* Render HUD before Panels (in this Case 'Panels' only does mean Inventory Panel). This is to avoid Entity Labels to be renderd above the Inventory Panel. Fixes #936
* This DCC frame size calculation seems useless
TBH there some to be some other overcomplicated things going on in
DCCAnimation but too tired to use brain right now.
* De-lint ecs branch
* bugfix: file_handle_resolver
* Add boot state to scenes, fix loading screen scene
* added label-button widget (#989)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* removed links to closed issues from code (#1005)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
* removed unused fields from d2player.GameControl.actionableRegions (#997)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
* d2ui.Frame refactor (#994)
* d2ui.Frame refactor
* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame
* d2ui.Frame refactor
* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: Tim Sarbin <tim.sarbin@gmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
* d2mpq refactored (#1020)
* d2mpq refactor
* d2mpq refactor last standing lint error
* d2mpq refactor: less linter noise
* d2mpq refactor: more linter issues
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: Tim Sarbin <tim.sarbin@gmail.com>
Co-authored-by: Hajime Hoshi <hajimehoshi@gmail.com>
Co-authored-by: gucio321 <73652197+gucio321@users.noreply.github.com>
Co-authored-by: Intyre <intyre@gmail.com>
Co-authored-by: ThomasChr <thomaschristlieb@hotmail.com>
Co-authored-by: Ziemas <ziemas@ziemas.se>
Co-authored-by: gravestench <dknuth0101@gmail.com>
965 lines
25 KiB
Go
965 lines
25 KiB
Go
// Package d2app contains the OpenDiablo2 application shell
|
|
package d2app
|
|
|
|
import (
|
|
"bytes"
|
|
"container/ring"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"image"
|
|
"image/gif"
|
|
"image/png"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/pprof"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data"
|
|
|
|
"github.com/pkg/profile"
|
|
"golang.org/x/image/colornames"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
|
ebiten2 "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio/ebiten"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2input"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render/ebiten"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2term"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2networking"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2script"
|
|
)
|
|
|
|
// these are used for debug print info
|
|
const (
|
|
fpsX, fpsY = 5, 565
|
|
memInfoX, memInfoY = 670, 5
|
|
debugLineHeight = 16
|
|
errMsgPadding = 20
|
|
)
|
|
|
|
// App represents the main application for the engine
|
|
type App struct {
|
|
lastTime float64
|
|
lastScreenAdvance float64
|
|
showFPS bool
|
|
timeScale float64
|
|
captureState captureState
|
|
capturePath string
|
|
captureFrames []*image.RGBA
|
|
gitBranch string
|
|
gitCommit string
|
|
language string
|
|
charset string
|
|
asset *d2asset.AssetManager
|
|
inputManager d2interface.InputManager
|
|
terminal d2interface.Terminal
|
|
scriptEngine *d2script.ScriptEngine
|
|
audio d2interface.AudioProvider
|
|
renderer d2interface.Renderer
|
|
screen *d2screen.ScreenManager
|
|
ui *d2ui.UIManager
|
|
tAllocSamples *ring.Ring
|
|
guiManager *d2gui.GuiManager
|
|
config *d2config.Configuration
|
|
*d2util.Logger
|
|
errorMessage error
|
|
*Options
|
|
}
|
|
|
|
// Options is used to store all of the app options that can be set with arguments
|
|
type Options struct {
|
|
Debug *bool
|
|
profiler *string
|
|
Server *d2networking.ServerOptions
|
|
LogLevel *d2util.LogLevel
|
|
}
|
|
|
|
const (
|
|
bytesToMegabyte = 1024 * 1024
|
|
nSamplesTAlloc = 100
|
|
debugPopN = 6
|
|
)
|
|
|
|
const (
|
|
appLoggerPrefix = "App"
|
|
)
|
|
|
|
// Create creates a new instance of the application
|
|
func Create(gitBranch, gitCommit string) *App {
|
|
logger := d2util.NewLogger()
|
|
logger.SetPrefix(appLoggerPrefix)
|
|
|
|
app := &App{
|
|
Logger: logger,
|
|
gitBranch: gitBranch,
|
|
gitCommit: gitCommit,
|
|
Options: &Options{
|
|
Server: &d2networking.ServerOptions{},
|
|
},
|
|
}
|
|
app.Infof("OpenDiablo2 - Open source Diablo 2 engine")
|
|
|
|
app.parseArguments()
|
|
|
|
app.SetLevel(*app.Options.LogLevel)
|
|
|
|
app.asset, app.errorMessage = d2asset.NewAssetManager(*app.Options.LogLevel)
|
|
|
|
return app
|
|
}
|
|
|
|
func updateNOOP() error {
|
|
return nil
|
|
}
|
|
|
|
func (a *App) startDedicatedServer() error {
|
|
min, max := d2networking.ServerMinPlayers, d2networking.ServerMaxPlayersDefault
|
|
maxPlayers := d2math.ClampInt(*a.Options.Server.MaxPlayers, min, max)
|
|
|
|
srvChanIn := make(chan int)
|
|
srvChanLog := make(chan string)
|
|
|
|
srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, *a.Options.LogLevel, maxPlayers)
|
|
if srvErr != nil {
|
|
return srvErr
|
|
}
|
|
|
|
c := make(chan os.Signal)
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM) // This traps Control-c to safely shut down the server
|
|
|
|
go func() {
|
|
<-c
|
|
srvChanIn <- d2networking.ServerEventStop
|
|
}()
|
|
|
|
for {
|
|
for data := range srvChanLog {
|
|
a.Info(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) loadEngine() error {
|
|
// Create our renderer
|
|
renderer, err := ebiten.CreateRenderer(a.config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.renderer = renderer
|
|
|
|
if a.errorMessage != nil {
|
|
return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2")
|
|
}
|
|
|
|
audio := ebiten2.CreateAudio(*a.Options.LogLevel, a.asset)
|
|
|
|
inputManager := d2input.NewInputManager()
|
|
|
|
term, err := d2term.New(inputManager)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
scriptEngine := d2script.CreateScriptEngine()
|
|
|
|
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio)
|
|
|
|
a.inputManager = inputManager
|
|
a.terminal = term
|
|
a.scriptEngine = scriptEngine
|
|
a.audio = audio
|
|
a.ui = uiManager
|
|
a.tAllocSamples = createZeroedRing(nSamplesTAlloc)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) parseArguments() {
|
|
const (
|
|
descProfile = "Profiles the program,\none of (cpu, mem, block, goroutine, trace, thread, mutex)"
|
|
descPlayers = "Sets the number of max players for the dedicated server"
|
|
descLogging = "Enables verbose logging. Log levels will include those below it.\n" +
|
|
" 0 disables log messages\n" +
|
|
" 1 shows fatal\n" +
|
|
" 2 shows error\n" +
|
|
" 3 shows warning\n" +
|
|
" 4 shows info\n" +
|
|
" 5 shows debug\n"
|
|
)
|
|
|
|
a.Options.profiler = flag.String("profile", "", descProfile)
|
|
a.Options.Server.Dedicated = flag.Bool("dedicated", false, "Starts a dedicated server")
|
|
a.Options.Server.MaxPlayers = flag.Int("players", 0, descPlayers)
|
|
a.Options.LogLevel = flag.Int("l", d2util.LogLevelDefault, descLogging)
|
|
showVersion := flag.Bool("v", false, "Show version")
|
|
showHelp := flag.Bool("h", false, "Show help")
|
|
|
|
flag.Usage = func() {
|
|
fmt.Printf("usage: %s [<flags>]\n\nFlags:\n", os.Args[0])
|
|
flag.PrintDefaults()
|
|
}
|
|
flag.Parse()
|
|
|
|
if *a.Options.LogLevel >= d2util.LogLevelUnspecified {
|
|
*a.Options.LogLevel = d2util.LogLevelDefault
|
|
}
|
|
|
|
if *showVersion {
|
|
a.Infof("version: OpenDiablo2 (%s %s)", a.gitBranch, a.gitCommit)
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *showHelp {
|
|
flag.Usage()
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// LoadConfig loads the OpenDiablo2 config file
|
|
func (a *App) LoadConfig() (*d2config.Configuration, error) {
|
|
// by now the, the loader has initialized and added our config dirs as sources...
|
|
configBaseName := filepath.Base(d2config.DefaultConfigPath())
|
|
|
|
configAsset, _ := a.asset.LoadAsset(configBaseName)
|
|
|
|
config := &d2config.Configuration{}
|
|
|
|
// create the default if not found
|
|
if configAsset == nil {
|
|
config = d2config.DefaultConfig()
|
|
|
|
fullPath := filepath.Join(config.Dir(), config.Base())
|
|
config.SetPath(fullPath)
|
|
|
|
a.Infof("creating default configuration file at %s...", fullPath)
|
|
|
|
saveErr := config.Save()
|
|
|
|
return config, saveErr
|
|
}
|
|
|
|
if err := json.NewDecoder(configAsset).Decode(config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config.SetPath(filepath.Join(configAsset.Source().Path(), configAsset.Path()))
|
|
|
|
a.Infof("loaded configuration file from %s", config.Path())
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// Run executes the application and kicks off the entire game process
|
|
func (a *App) Run() (err error) {
|
|
// add our possible config directories
|
|
_, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()))
|
|
_, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()))
|
|
|
|
if a.config, err = a.LoadConfig(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// start profiler if argument was supplied
|
|
if len(*a.Options.profiler) > 0 {
|
|
profiler := enableProfiler(*a.Options.profiler, a)
|
|
if profiler != nil {
|
|
defer profiler.Stop()
|
|
}
|
|
}
|
|
|
|
// start the server if `--listen` option was supplied
|
|
if *a.Options.Server.Dedicated {
|
|
if err := a.startDedicatedServer(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := a.loadEngine(); err != nil {
|
|
a.renderer.ShowPanicScreen(err.Error())
|
|
return err
|
|
}
|
|
|
|
windowTitle := fmt.Sprintf("OpenDiablo2 (%s)", a.gitBranch)
|
|
|
|
// If we fail to initialize, we will show the error screen
|
|
if err := a.initialize(); err != nil {
|
|
if a.errorMessage == nil {
|
|
a.errorMessage = err // if there was an error during init, don't clobber it
|
|
}
|
|
|
|
gameErr := a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, windowTitle)
|
|
if gameErr != nil {
|
|
return gameErr
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
a.ToMainMenu()
|
|
|
|
if err := a.renderer.Run(a.update, a.advance, 800, 600, windowTitle); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) initialize() error {
|
|
if err := a.initConfig(a.config); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.initLanguage()
|
|
|
|
if err := a.initDataDictionaries(); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.timeScale = 1.0
|
|
a.lastTime = d2util.Now()
|
|
a.lastScreenAdvance = a.lastTime
|
|
|
|
a.renderer.SetWindowIcon("d2logo.png")
|
|
a.terminal.BindLogger()
|
|
|
|
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 _, cmd := range terminalCommands {
|
|
if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil {
|
|
a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error())
|
|
}
|
|
}
|
|
|
|
gui, err := d2gui.CreateGuiManager(a.asset, *a.Options.LogLevel, a.inputManager)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.guiManager = gui
|
|
|
|
a.screen = d2screen.NewScreenManager(a.ui, *a.Options.LogLevel, a.guiManager)
|
|
|
|
a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)
|
|
|
|
if err := a.loadStrings(); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.ui.Initialize()
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
fmtErrSourceNotFound = `file not found: %s
|
|
|
|
Please check your config file at %s
|
|
|
|
Also, verify that the MPQ files exist at %s
|
|
|
|
Capitalization in the file name matters.
|
|
`
|
|
)
|
|
|
|
func (a *App) initConfig(config *d2config.Configuration) error {
|
|
a.config = config
|
|
|
|
for _, mpqName := range a.config.MpqLoadOrder {
|
|
cleanDir := filepath.Clean(a.config.MpqPath)
|
|
srcPath := filepath.Join(cleanDir, mpqName)
|
|
|
|
_, err := a.asset.AddSource(srcPath)
|
|
if err != nil {
|
|
// nolint:stylecheck // we want a multiline error message here..
|
|
return fmt.Errorf(fmtErrSourceNotFound, srcPath, a.config.Path(), a.config.MpqPath)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) initLanguage() {
|
|
a.language = a.asset.LoadLanguage(d2resource.LocalLanguage)
|
|
a.asset.Loader.SetLanguage(&a.language)
|
|
|
|
a.charset = d2resource.GetFontCharset(a.language)
|
|
a.asset.Loader.SetCharset(&a.charset)
|
|
}
|
|
|
|
func (a *App) initDataDictionaries() error {
|
|
dictPaths := []string{
|
|
d2resource.LevelType, d2resource.LevelPreset, d2resource.LevelWarp,
|
|
d2resource.ObjectType, d2resource.ObjectDetails, d2resource.Weapons,
|
|
d2resource.Armor, d2resource.Misc, d2resource.Books, d2resource.ItemTypes,
|
|
d2resource.UniqueItems, d2resource.Missiles, d2resource.SoundSettings,
|
|
d2resource.MonStats, d2resource.MonStats2, d2resource.MonPreset,
|
|
d2resource.MonProp, d2resource.MonType, d2resource.MonMode,
|
|
d2resource.MagicPrefix, d2resource.MagicSuffix, d2resource.ItemStatCost,
|
|
d2resource.ItemRatio, d2resource.StorePage, d2resource.Overlays,
|
|
d2resource.CharStats, d2resource.Hireling, d2resource.Experience,
|
|
d2resource.Gems, d2resource.QualityItems, d2resource.Runes,
|
|
d2resource.DifficultyLevels, d2resource.AutoMap, d2resource.LevelDetails,
|
|
d2resource.LevelMaze, d2resource.LevelSubstitutions, d2resource.CubeRecipes,
|
|
d2resource.SuperUniques, d2resource.Inventory, d2resource.Skills,
|
|
d2resource.SkillCalc, d2resource.MissileCalc, d2resource.Properties,
|
|
d2resource.SkillDesc, d2resource.BodyLocations, d2resource.Sets,
|
|
d2resource.SetItems, d2resource.AutoMagic, d2resource.TreasureClass,
|
|
d2resource.TreasureClassEx, d2resource.States, d2resource.SoundEnvirons,
|
|
d2resource.Shrines, d2resource.ElemType, d2resource.PlrMode,
|
|
d2resource.PetType, d2resource.NPC, d2resource.MonsterUniqueModifier,
|
|
d2resource.MonsterEquipment, d2resource.UniqueAppellation, d2resource.MonsterLevel,
|
|
d2resource.MonsterSound, d2resource.MonsterSequence, d2resource.PlayerClass,
|
|
d2resource.MonsterPlacement, d2resource.ObjectGroup, d2resource.CompCode,
|
|
d2resource.MonsterAI, d2resource.RarePrefix, d2resource.RareSuffix,
|
|
d2resource.Events, d2resource.Colors, d2resource.ArmorType,
|
|
d2resource.WeaponClass, d2resource.PlayerType, d2resource.Composite,
|
|
d2resource.HitClass, d2resource.UniquePrefix, d2resource.UniqueSuffix,
|
|
d2resource.CubeModifier, d2resource.CubeType, d2resource.HirelingDescription,
|
|
d2resource.LowQualityItems,
|
|
}
|
|
|
|
a.Info("Initializing asset manager")
|
|
|
|
for _, path := range dictPaths {
|
|
err := a.asset.LoadRecords(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := a.initAnimationData(d2resource.AnimationData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
fmtLoadAnimData = "loading animation data from: %s"
|
|
)
|
|
|
|
func (a *App) initAnimationData(path string) error {
|
|
animDataBytes, err := a.asset.LoadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.Debugf(fmtLoadAnimData, path)
|
|
|
|
animData := d2data.LoadAnimationData(animDataBytes)
|
|
|
|
a.Infof("Loaded %d animation data records", len(animData))
|
|
|
|
a.asset.Records.Animation.Data = animData
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) loadStrings() error {
|
|
tablePaths := []string{
|
|
d2resource.PatchStringTable,
|
|
d2resource.ExpansionStringTable,
|
|
d2resource.StringTable,
|
|
}
|
|
|
|
for _, tablePath := range tablePaths {
|
|
_, err := a.asset.LoadStringTable(tablePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) renderDebug(target d2interface.Surface) {
|
|
if !a.showFPS {
|
|
return
|
|
}
|
|
|
|
vsyncEnabled := a.renderer.GetVSyncEnabled()
|
|
fps := a.renderer.CurrentFPS()
|
|
cx, cy := a.renderer.GetCursorPos()
|
|
|
|
target.PushTranslation(fpsX, fpsY)
|
|
target.DrawTextf("vsync:" + strconv.FormatBool(vsyncEnabled) + "\nFPS:" + strconv.Itoa(int(fps)))
|
|
target.Pop()
|
|
|
|
var m runtime.MemStats
|
|
|
|
runtime.ReadMemStats(&m)
|
|
target.PushTranslation(memInfoX, memInfoY)
|
|
target.DrawTextf("Alloc " + strconv.FormatInt(int64(m.Alloc)/bytesToMegabyte, 10))
|
|
target.PushTranslation(0, debugLineHeight)
|
|
target.DrawTextf("TAlloc/s " + strconv.FormatFloat(a.allocRate(m.TotalAlloc, fps), 'f', 2, 64))
|
|
target.PushTranslation(0, debugLineHeight)
|
|
target.DrawTextf("Pause " + strconv.FormatInt(int64(m.PauseTotalNs/bytesToMegabyte), 10))
|
|
target.PushTranslation(0, debugLineHeight)
|
|
target.DrawTextf("HeapSys " + strconv.FormatInt(int64(m.HeapSys/bytesToMegabyte), 10))
|
|
target.PushTranslation(0, debugLineHeight)
|
|
target.DrawTextf("NumGC " + strconv.FormatInt(int64(m.NumGC), 10))
|
|
target.PushTranslation(0, debugLineHeight)
|
|
target.DrawTextf("Coords " + strconv.FormatInt(int64(cx), 10) + "," + strconv.FormatInt(int64(cy), 10))
|
|
target.PopN(debugPopN)
|
|
}
|
|
|
|
func (a *App) renderCapture(target d2interface.Surface) error {
|
|
cleanupCapture := func() {
|
|
a.captureState = captureStateNone
|
|
a.capturePath = ""
|
|
a.captureFrames = nil
|
|
}
|
|
|
|
switch a.captureState {
|
|
case captureStateFrame:
|
|
defer cleanupCapture()
|
|
|
|
if err := a.doCaptureFrame(target); err != nil {
|
|
return err
|
|
}
|
|
case captureStateGif:
|
|
a.doCaptureGif(target)
|
|
case captureStateNone:
|
|
if len(a.captureFrames) > 0 {
|
|
defer cleanupCapture()
|
|
|
|
if err := a.convertFramesToGif(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) render(target d2interface.Surface) {
|
|
a.screen.Render(target)
|
|
a.ui.Render(target)
|
|
|
|
if err := a.guiManager.Render(target); err != nil {
|
|
return
|
|
}
|
|
|
|
a.renderDebug(target)
|
|
|
|
if err := a.renderCapture(target); err != nil {
|
|
return
|
|
}
|
|
|
|
if err := a.terminal.Render(target); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (a *App) advance() error {
|
|
current := d2util.Now()
|
|
elapsedUnscaled := current - a.lastTime
|
|
elapsed := elapsedUnscaled * a.timeScale
|
|
|
|
a.lastTime = current
|
|
|
|
elapsedLastScreenAdvance := (current - a.lastScreenAdvance) * a.timeScale
|
|
a.lastScreenAdvance = current
|
|
|
|
if err := a.screen.Advance(elapsedLastScreenAdvance); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.ui.Advance(elapsed)
|
|
|
|
if err := a.inputManager.Advance(elapsed, current); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.guiManager.Advance(elapsed); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.terminal.Advance(elapsedUnscaled); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) update(target d2interface.Surface) error {
|
|
a.render(target)
|
|
|
|
if target.GetDepth() > 0 {
|
|
return errors.New("detected surface stack leak")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
|
|
a.tAllocSamples.Value = totalAlloc
|
|
a.tAllocSamples = a.tAllocSamples.Next()
|
|
deltaAllocPerFrame := float64(totalAlloc-a.tAllocSamples.Value.(uint64)) / nSamplesTAlloc
|
|
|
|
return deltaAllocPerFrame * fps / bytesToMegabyte
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
fileOut, err := os.Create("./pprof/heap.pprof")
|
|
if err != nil {
|
|
a.Error(err.Error())
|
|
}
|
|
|
|
if err := pprof.WriteHeapProfile(fileOut); err != nil {
|
|
a.Fatal(err.Error())
|
|
}
|
|
|
|
if err := fileOut.Close(); err != nil {
|
|
a.Fatal(err.Error())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) evalJS(args []string) error {
|
|
val, err := a.scriptEngine.Eval(args[0])
|
|
if err != nil {
|
|
a.terminal.Errorf(err.Error())
|
|
return nil
|
|
}
|
|
|
|
a.Info("%s" + val)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) toggleFullScreen([]string) error {
|
|
fullscreen := !a.renderer.IsFullScreen()
|
|
a.renderer.SetFullScreen(fullscreen)
|
|
a.terminal.Infof("fullscreen is now: %v", fullscreen)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) setupCaptureFrame(args []string) error {
|
|
a.captureState = captureStateFrame
|
|
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
|
|
}
|
|
|
|
screenshot := target.Screenshot()
|
|
if err := png.Encode(fp, screenshot); err != nil {
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (a *App) doCaptureGif(target d2interface.Surface) {
|
|
screenshot := target.Screenshot()
|
|
a.captureFrames = append(a.captureFrames, screenshot)
|
|
}
|
|
|
|
func (a *App) convertFramesToGif() error {
|
|
fp, err := os.Create(a.capturePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err := fp.Close(); err != nil {
|
|
a.Fatal(err.Error())
|
|
}
|
|
}()
|
|
|
|
var (
|
|
framesTotal = len(a.captureFrames)
|
|
framesPal = make([]*image.Paletted, framesTotal)
|
|
frameDelays = make([]int, framesTotal)
|
|
framesPerCPU = framesTotal / runtime.NumCPU()
|
|
)
|
|
|
|
var waitGroup sync.WaitGroup
|
|
|
|
for i := 0; i < framesTotal; i += framesPerCPU {
|
|
waitGroup.Add(1)
|
|
|
|
go func(start, end int) {
|
|
defer waitGroup.Done()
|
|
|
|
for j := start; j < end; j++ {
|
|
var buffer bytes.Buffer
|
|
if err := gif.Encode(&buffer, a.captureFrames[j], nil); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
framePal, err := gif.Decode(&buffer)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
framesPal[j] = framePal.(*image.Paletted)
|
|
frameDelays[j] = 5
|
|
}
|
|
}(i, d2math.MinInt(i+framesPerCPU, framesTotal))
|
|
}
|
|
|
|
waitGroup.Wait()
|
|
|
|
if err := gif.EncodeAll(fp, &gif.GIF{Image: framesPal, Delay: frameDelays}); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.Infof("saved animation to %s", a.capturePath)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) startAnimationCapture(args []string) error {
|
|
a.captureState = captureStateGif
|
|
a.capturePath = args[0]
|
|
a.captureFrames = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) stopAnimationCapture([]string) error {
|
|
a.captureState = captureStateNone
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) toggleVsync([]string) error {
|
|
vsync := !a.renderer.GetVSyncEnabled()
|
|
a.renderer.SetVSyncEnabled(vsync)
|
|
a.terminal.Infof("vsync is now: %v", vsync)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) toggleFpsCounter([]string) error {
|
|
a.showFPS = !a.showFPS
|
|
a.terminal.Infof("fps counter is now: %v", a.showFPS)
|
|
|
|
return nil
|
|
}
|
|
|
|
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([]string) error {
|
|
os.Exit(0)
|
|
return nil
|
|
}
|
|
|
|
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 {
|
|
r := ring.New(n)
|
|
for i := 0; i < n; i++ {
|
|
r.Value = uint64(0)
|
|
r = r.Next()
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func enableProfiler(profileOption string, a *App) interface{ Stop() } {
|
|
var options []func(*profile.Profile)
|
|
|
|
switch strings.ToLower(strings.Trim(profileOption, " ")) {
|
|
case "cpu":
|
|
a.Logger.Debug("CPU profiling is enabled.")
|
|
|
|
options = append(options, profile.CPUProfile)
|
|
case "mem":
|
|
a.Logger.Debug("Memory profiling is enabled.")
|
|
|
|
options = append(options, profile.MemProfile)
|
|
case "block":
|
|
a.Logger.Debug("Block profiling is enabled.")
|
|
|
|
options = append(options, profile.BlockProfile)
|
|
case "goroutine":
|
|
a.Logger.Debug("Goroutine profiling is enabled.")
|
|
|
|
options = append(options, profile.GoroutineProfile)
|
|
case "trace":
|
|
a.Logger.Debug("Trace profiling is enabled.")
|
|
|
|
options = append(options, profile.TraceProfile)
|
|
case "thread":
|
|
a.Logger.Debug("Thread creation profiling is enabled.")
|
|
|
|
options = append(options, profile.ThreadcreationProfile)
|
|
case "mutex":
|
|
a.Logger.Debug("Mutex profiling is enabled.")
|
|
|
|
options = append(options, profile.MutexProfile)
|
|
}
|
|
|
|
options = append(options, profile.ProfilePath("./pprof/"))
|
|
|
|
if len(options) > 1 {
|
|
return profile.Start(options...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) updateInitError(target d2interface.Surface) error {
|
|
target.Clear(colornames.Darkred)
|
|
target.PushTranslation(errMsgPadding, errMsgPadding)
|
|
target.DrawTextf(a.errorMessage.Error())
|
|
|
|
return nil
|
|
}
|
|
|
|
// ToMainMenu forces the game to transition to the Main Menu
|
|
func (a *App) ToMainMenu(errorMessageOptional ...string) {
|
|
buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit}
|
|
|
|
mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo,
|
|
*a.Options.LogLevel, errorMessageOptional...)
|
|
if err != nil {
|
|
a.Error(err.Error())
|
|
return
|
|
}
|
|
|
|
a.screen.SetNextScreen(mainMenu)
|
|
}
|
|
|
|
// ToSelectHero forces the game to transition to the Select Hero (create character) screen
|
|
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
|
|
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, *a.Options.LogLevel, host)
|
|
if err != nil {
|
|
a.Error(err.Error())
|
|
return
|
|
}
|
|
|
|
a.screen.SetNextScreen(selectHero)
|
|
}
|
|
|
|
// ToCreateGame forces the game to transition to the Create Game screen
|
|
func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) {
|
|
gameClient, err := d2client.Create(connType, a.asset, *a.Options.LogLevel, a.scriptEngine)
|
|
if err != nil {
|
|
a.Error(err.Error())
|
|
}
|
|
|
|
if err = gameClient.Open(host, filePath); err != nil {
|
|
errorMessage := fmt.Sprintf("can not connect to the host: %s", host)
|
|
a.Error(errorMessage)
|
|
a.ToMainMenu(errorMessage)
|
|
} else {
|
|
game, err := d2gamescreen.CreateGame(
|
|
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, *a.Options.LogLevel, a.guiManager,
|
|
)
|
|
if err != nil {
|
|
a.Error(err.Error())
|
|
}
|
|
|
|
a.screen.SetNextScreen(game)
|
|
}
|
|
}
|
|
|
|
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
|
|
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
|
|
characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
|
|
a.audio, a.ui, connType, *a.Options.LogLevel, connHost)
|
|
if err != nil {
|
|
a.Errorf("unable to create character select screen: %s", err)
|
|
}
|
|
|
|
a.screen.SetNextScreen(characterSelect)
|
|
}
|
|
|
|
// ToMapEngineTest forces the game to transition to the map engine test screen
|
|
func (a *App) ToMapEngineTest(region, level int) {
|
|
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio,
|
|
*a.Options.LogLevel, a.screen)
|
|
if err != nil {
|
|
a.Error(err.Error())
|
|
return
|
|
}
|
|
|
|
a.screen.SetNextScreen(met)
|
|
}
|
|
|
|
// ToCredits forces the game to transition to the credits screen
|
|
func (a *App) ToCredits() {
|
|
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, *a.Options.LogLevel, a.ui))
|
|
}
|
|
|
|
// ToCinematics forces the game to transition to the cinematics menu
|
|
func (a *App) ToCinematics() {
|
|
a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, *a.Options.LogLevel, a.ui))
|
|
}
|