1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2025-02-02 22:57:04 -05:00

Backmerge master into ecs (#1021)

* 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>
This commit is contained in:
Ian 2021-01-09 00:25:44 -08:00 committed by GitHub
parent 0dee6518b4
commit 9121209f86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 2729 additions and 2395 deletions

View File

@ -1,10 +1,9 @@
---
name: pull_request name: pull_request
"on": [pull_request] "on": [pull_request]
jobs: jobs:
build: build:
name: Build name: ''
runs-on: ubuntu-latest runs-on: self-hosted
continue-on-error: true continue-on-error: true
steps: steps:
- name: Set up Go 1.14 - name: Set up Go 1.14

View File

@ -1,39 +0,0 @@
---
name: build
"on":
push:
branches:
- master
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.14
uses: actions/setup-go@v2.1.3
with:
go-version: 1.14
id: go
- name: Check out code
uses: actions/checkout@v2.3.4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y xvfb libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
- name: Run golangci-lint
continue-on-error: false
uses: golangci/golangci-lint-action@v2.3.0
with:
version: v1.32
- name: Run tests
env:
DISPLAY: ":99.0"
run: |
xvfb-run --auto-servernum go test -v -race ./...
- name: Build binary
run: go build .

View File

@ -25,7 +25,8 @@ ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
## Status ## Status
At the moment (october 2020) the game starts, you can select any character and run around Act1 town. At the moment (december 2020) the game starts, you can select any character and run around Act1 town.
You can also open any of the game's panels.
Much work has been made in the background, but a lot of work still has to be done for the game to be playable. Much work has been made in the background, but a lot of work still has to be done for the game to be playable.
@ -128,6 +129,8 @@ which will be updated over time with new requirements.
![Inventory Window](docs/Inventory.png) ![Inventory Window](docs/Inventory.png)
![Game Panels](docs/game_panels.png)
## Additional Credits ## Additional Credits
- Diablo2 Logo - Diablo2 Logo

View File

@ -6,6 +6,7 @@ import (
"container/ring" "container/ring"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"image" "image"
"image/gif" "image/gif"
@ -24,7 +25,6 @@ import (
"github.com/pkg/profile" "github.com/pkg/profile"
"golang.org/x/image/colornames" "golang.org/x/image/colornames"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
@ -85,17 +85,10 @@ type App struct {
// Options is used to store all of the app options that can be set with arguments // Options is used to store all of the app options that can be set with arguments
type Options struct { type Options struct {
printVersion *bool Debug *bool
Debug *bool profiler *string
profiler *string Server *d2networking.ServerOptions
Server *d2networking.ServerOptions LogLevel *d2util.LogLevel
LogLevel *d2util.LogLevel
}
type bindTerminalEntry struct {
name string
description string
action interface{}
} }
const ( const (
@ -110,21 +103,24 @@ const (
// Create creates a new instance of the application // Create creates a new instance of the application
func Create(gitBranch, gitCommit string) *App { func Create(gitBranch, gitCommit string) *App {
assetManager, assetError := d2asset.NewAssetManager() logger := d2util.NewLogger()
logger.SetPrefix(appLoggerPrefix)
app := &App{ app := &App{
Logger: logger,
gitBranch: gitBranch, gitBranch: gitBranch,
gitCommit: gitCommit, gitCommit: gitCommit,
asset: assetManager,
Options: &Options{ Options: &Options{
Server: &d2networking.ServerOptions{}, Server: &d2networking.ServerOptions{},
}, },
errorMessage: assetError,
} }
app.Infof("OpenDiablo2 - Open source Diablo 2 engine")
app.Logger = d2util.NewLogger() app.parseArguments()
app.Logger.SetPrefix(appLoggerPrefix)
app.Logger.SetLevel(d2util.LogLevelNone) app.SetLevel(*app.Options.LogLevel)
app.asset, app.errorMessage = d2asset.NewAssetManager(*app.Options.LogLevel)
return app return app
} }
@ -140,7 +136,7 @@ func (a *App) startDedicatedServer() error {
srvChanIn := make(chan int) srvChanIn := make(chan int)
srvChanLog := make(chan string) srvChanLog := make(chan string)
srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, a.config.LogLevel, maxPlayers) srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, *a.Options.LogLevel, maxPlayers)
if srvErr != nil { if srvErr != nil {
return srvErr return srvErr
} }
@ -173,15 +169,7 @@ func (a *App) loadEngine() error {
return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2") return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2")
} }
// if the log level was specified at the command line, use it audio := ebiten2.CreateAudio(*a.Options.LogLevel, a.asset)
logLevel := *a.Options.LogLevel
if logLevel == d2util.LogLevelUnspecified {
logLevel = a.config.LogLevel
}
a.asset.SetLogLevel(logLevel)
audio := ebiten2.CreateAudio(a.config.LogLevel, a.asset)
inputManager := d2input.NewInputManager() inputManager := d2input.NewInputManager()
@ -190,14 +178,9 @@ func (a *App) loadEngine() error {
return err return err
} }
err = a.asset.BindTerminalCommands(term)
if err != nil {
return err
}
scriptEngine := d2script.CreateScriptEngine() scriptEngine := d2script.CreateScriptEngine()
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, a.config.LogLevel, audio) uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio)
a.inputManager = inputManager a.inputManager = inputManager
a.terminal = term a.terminal = term
@ -206,50 +189,48 @@ func (a *App) loadEngine() error {
a.ui = uiManager a.ui = uiManager
a.tAllocSamples = createZeroedRing(nSamplesTAlloc) a.tAllocSamples = createZeroedRing(nSamplesTAlloc)
if a.gitBranch == "" {
a.gitBranch = "Local Build"
}
return nil return nil
} }
func (a *App) parseArguments() { func (a *App) parseArguments() {
const ( const (
versionArg = "version" descProfile = "Profiles the program,\none of (cpu, mem, block, goroutine, trace, thread, mutex)"
versionShort = 'v' descPlayers = "Sets the number of max players for the dedicated server"
versionDesc = "Prints the version of the app" descLogging = "Enables verbose logging. Log levels will include those below it.\n" +
" 0 disables log messages\n" +
profilerArg = "profile" " 1 shows fatal\n" +
profilerDesc = "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)" " 2 shows error\n" +
" 3 shows warning\n" +
serverArg = "dedicated" " 4 shows info\n" +
serverShort = 'd' " 5 shows debug\n"
serverDesc = "Starts a dedicated server"
playersArg = "players"
playersDesc = "Sets the number of max players for the dedicated server"
loggingArg = "loglevel"
loggingShort = 'l'
loggingDesc = "Enables verbose logging. Log levels will include those below it. " +
"0 disables log messages, " +
"1 shows errors, " +
"2 shows warnings, " +
"3 shows info, " +
"4 shows debug" +
"5 uses value from config file (default)"
) )
a.Options.profiler = kingpin.Flag(profilerArg, profilerDesc).String() a.Options.profiler = flag.String("profile", "", descProfile)
a.Options.Server.Dedicated = kingpin.Flag(serverArg, serverDesc).Short(serverShort).Bool() a.Options.Server.Dedicated = flag.Bool("dedicated", false, "Starts a dedicated server")
a.Options.printVersion = kingpin.Flag(versionArg, versionDesc).Short(versionShort).Bool() a.Options.Server.MaxPlayers = flag.Int("players", 0, descPlayers)
a.Options.Server.MaxPlayers = kingpin.Flag(playersArg, playersDesc).Int() a.Options.LogLevel = flag.Int("l", d2util.LogLevelDefault, descLogging)
a.Options.LogLevel = kingpin.Flag(loggingArg, loggingDesc). showVersion := flag.Bool("v", false, "Show version")
Short(loggingShort). showHelp := flag.Bool("h", false, "Show help")
Default(strconv.Itoa(d2util.LogLevelUnspecified)).
Int()
kingpin.Parse() 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 // LoadConfig loads the OpenDiablo2 config file
@ -287,45 +268,15 @@ func (a *App) LoadConfig() (*d2config.Configuration, error) {
} }
// Run executes the application and kicks off the entire game process // Run executes the application and kicks off the entire game process
func (a *App) Run() error { func (a *App) Run() (err error) {
a.parseArguments()
// add our possible config directories // add our possible config directories
_, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath())) _, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()))
_, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath())) _, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()))
config, err := a.LoadConfig() if a.config, err = a.LoadConfig(); err != nil {
if err != nil {
return err return err
} }
a.config = config
a.asset.SetLogLevel(config.LogLevel)
// print version and exit if `--version` was supplied
if *a.Options.printVersion {
fmtVersion := "OpenDiablo2 (%s %s)"
if a.gitBranch == "" {
a.gitBranch = "local"
}
if a.gitCommit == "" {
a.gitCommit = "build"
}
fmt.Printf(fmtVersion, a.gitBranch, a.gitCommit)
os.Exit(0)
}
logLevel := *a.Options.LogLevel
if logLevel == d2util.LogLevelUnspecified {
logLevel = a.config.LogLevel
}
a.asset.SetLogLevel(logLevel)
// start profiler if argument was supplied // start profiler if argument was supplied
if len(*a.Options.profiler) > 0 { if len(*a.Options.profiler) > 0 {
profiler := enableProfiler(*a.Options.profiler, a) profiler := enableProfiler(*a.Options.profiler, a)
@ -389,36 +340,39 @@ func (a *App) initialize() error {
a.renderer.SetWindowIcon("d2logo.png") a.renderer.SetWindowIcon("d2logo.png")
a.terminal.BindLogger() a.terminal.BindLogger()
terminalActions := [...]bindTerminalEntry{ terminalCommands := []struct {
{"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap}, name string
{"fullscreen", "toggles fullscreen", a.toggleFullScreen}, desc string
{"capframe", "captures a still frame", a.setupCaptureFrame}, args []string
{"capgifstart", "captures an animation (start)", a.startAnimationCapture}, fn func(args []string) error
{"capgifstop", "captures an animation (stop)", a.stopAnimationCapture}, }{
{"vsync", "toggles vsync", a.toggleVsync}, {"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap},
{"fps", "toggle fps counter", a.toggleFpsCounter}, {"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen},
{"timescale", "set scalar for elapsed time", a.setTimeScale}, {"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame},
{"quit", "exits the game", a.quitGame}, {"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture},
{"screen-gui", "enters the gui playground screen", a.enterGuiPlayground}, {"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture},
{"js", "eval JS scripts", a.evalJS}, {"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 { for _, cmd := range terminalCommands {
action := &terminalActions[idx] 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())
if err := a.terminal.BindAction(action.name, action.description, action.action); err != nil {
a.Fatal(err.Error())
} }
} }
gui, err := d2gui.CreateGuiManager(a.asset, a.config.LogLevel, a.inputManager) gui, err := d2gui.CreateGuiManager(a.asset, *a.Options.LogLevel, a.inputManager)
if err != nil { if err != nil {
return err return err
} }
a.guiManager = gui a.guiManager = gui
a.screen = d2screen.NewScreenManager(a.ui, a.config.LogLevel, a.guiManager) a.screen = d2screen.NewScreenManager(a.ui, *a.Options.LogLevel, a.guiManager)
a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume) a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)
@ -682,7 +636,7 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
return deltaAllocPerFrame * fps / bytesToMegabyte 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.Stat("./pprof/"); os.IsNotExist(err) {
if err := os.Mkdir("./pprof/", 0750); err != nil { if err := os.Mkdir("./pprof/", 0750); err != nil {
a.Fatal(err.Error()) a.Fatal(err.Error())
@ -701,48 +655,56 @@ func (a *App) dumpHeap() {
if err := fileOut.Close(); err != nil { if err := fileOut.Close(); err != nil {
a.Fatal(err.Error()) a.Fatal(err.Error())
} }
return nil
} }
func (a *App) evalJS(code string) { func (a *App) evalJS(args []string) error {
val, err := a.scriptEngine.Eval(code) val, err := a.scriptEngine.Eval(args[0])
if err != nil { if err != nil {
a.terminal.OutputErrorf("%s", err) a.terminal.Errorf(err.Error())
return return nil
} }
a.Info("%s" + val) a.Info("%s" + val)
return nil
} }
func (a *App) toggleFullScreen() { func (a *App) toggleFullScreen([]string) error {
fullscreen := !a.renderer.IsFullScreen() fullscreen := !a.renderer.IsFullScreen()
a.renderer.SetFullScreen(fullscreen) 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.captureState = captureStateFrame
a.capturePath = path a.capturePath = args[0]
a.captureFrames = nil a.captureFrames = nil
return nil
} }
func (a *App) doCaptureFrame(target d2interface.Surface) error { func (a *App) doCaptureFrame(target d2interface.Surface) error {
fp, err := os.Create(a.capturePath) fp, err := os.Create(a.capturePath)
if err != nil { if err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return err return err
} }
defer func() {
if err := fp.Close(); err != nil {
a.Fatal(err.Error())
}
}()
screenshot := target.Screenshot() screenshot := target.Screenshot()
if err := png.Encode(fp, screenshot); err != nil { if err := png.Encode(fp, screenshot); err != nil {
return err return err
} }
a.Info(fmt.Sprintf("saved frame to %s", a.capturePath)) if err := fp.Close(); err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return nil
}
a.terminal.Infof("saved frame to %s", a.capturePath)
return nil return nil
} }
@ -802,47 +764,61 @@ func (a *App) convertFramesToGif() error {
return err return err
} }
a.Info(fmt.Sprintf("saved animation to %s", a.capturePath)) a.Infof("saved animation to %s", a.capturePath)
return nil return nil
} }
func (a *App) startAnimationCapture(path string) { func (a *App) startAnimationCapture(args []string) error {
a.captureState = captureStateGif a.captureState = captureStateGif
a.capturePath = path a.capturePath = args[0]
a.captureFrames = nil a.captureFrames = nil
return nil
} }
func (a *App) stopAnimationCapture() { func (a *App) stopAnimationCapture([]string) error {
a.captureState = captureStateNone a.captureState = captureStateNone
return nil
} }
func (a *App) toggleVsync() { func (a *App) toggleVsync([]string) error {
vsync := !a.renderer.GetVSyncEnabled() vsync := !a.renderer.GetVSyncEnabled()
a.renderer.SetVSyncEnabled(vsync) 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.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) { func (a *App) setTimeScale(args []string) error {
if timeScale <= 0 { timeScale, err := strconv.ParseFloat(args[0], 64)
a.terminal.OutputErrorf("invalid time scale value") if err != nil || timeScale <= 0 {
} else { a.terminal.Errorf("invalid time scale value")
a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale) return nil
a.timeScale = timeScale
} }
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) 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.config.LogLevel, a.asset)) a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset))
return nil
} }
func createZeroedRing(n int) *ring.Ring { func createZeroedRing(n int) *ring.Ring {
@ -911,7 +887,7 @@ func (a *App) ToMainMenu(errorMessageOptional ...string) {
buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit} 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, mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo,
a.config.LogLevel, errorMessageOptional...) *a.Options.LogLevel, errorMessageOptional...)
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
return return
@ -922,7 +898,7 @@ func (a *App) ToMainMenu(errorMessageOptional ...string) {
// ToSelectHero forces the game to transition to the Select Hero (create character) screen // ToSelectHero forces the game to transition to the Select Hero (create character) screen
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) { func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, a.config.LogLevel, host) selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, *a.Options.LogLevel, host)
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
return return
@ -933,18 +909,18 @@ func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType,
// ToCreateGame forces the game to transition to the Create Game screen // ToCreateGame forces the game to transition to the Create Game screen
func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) { func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) {
gameClient, err := d2client.Create(connType, a.asset, a.config.LogLevel, a.scriptEngine) gameClient, err := d2client.Create(connType, a.asset, *a.Options.LogLevel, a.scriptEngine)
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
} }
if err = gameClient.Open(host, filePath); err != nil { if err = gameClient.Open(host, filePath); err != nil {
errorMessage := fmt.Sprintf("can not connect to the host: %s", host) errorMessage := fmt.Sprintf("can not connect to the host: %s", host)
fmt.Println(errorMessage) a.Error(errorMessage)
a.ToMainMenu(errorMessage) a.ToMainMenu(errorMessage)
} else { } else {
game, err := d2gamescreen.CreateGame( game, err := d2gamescreen.CreateGame(
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, a.config.LogLevel, a.guiManager, a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, *a.Options.LogLevel, a.guiManager,
) )
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
@ -957,9 +933,9 @@ func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.Clie
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen // ToCharacterSelect forces the game to transition to the Character Select (load character) screen
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) { func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager, characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
a.audio, a.ui, connType, a.config.LogLevel, connHost) a.audio, a.ui, connType, *a.Options.LogLevel, connHost)
if err != nil { if err != nil {
fmt.Printf("unable to create character select screen: %s", err) a.Errorf("unable to create character select screen: %s", err)
} }
a.screen.SetNextScreen(characterSelect) a.screen.SetNextScreen(characterSelect)
@ -968,7 +944,7 @@ func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnection
// ToMapEngineTest forces the game to transition to the map engine test screen // ToMapEngineTest forces the game to transition to the map engine test screen
func (a *App) ToMapEngineTest(region, level int) { func (a *App) ToMapEngineTest(region, level int) {
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio, met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio,
a.config.LogLevel, a.screen) *a.Options.LogLevel, a.screen)
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
return return
@ -979,10 +955,10 @@ func (a *App) ToMapEngineTest(region, level int) {
// ToCredits forces the game to transition to the credits screen // ToCredits forces the game to transition to the credits screen
func (a *App) ToCredits() { func (a *App) ToCredits() {
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, a.config.LogLevel, a.ui)) a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, *a.Options.LogLevel, a.ui))
} }
// ToCinematics forces the game to transition to the cinematics menu // ToCinematics forces the game to transition to the cinematics menu
func (a *App) ToCinematics() { func (a *App) ToCinematics() {
a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, a.config.LogLevel, a.ui)) a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, *a.Options.LogLevel, a.ui))
} }

View File

@ -4,12 +4,6 @@ import (
"io" "io"
) )
const (
bytesPerInt16 = 2
bytesPerInt32 = 4
bytesPerInt64 = 8
)
// StreamReader allows you to read data from a byte array in various formats // StreamReader allows you to read data from a byte array in various formats
type StreamReader struct { type StreamReader struct {
data []byte data []byte
@ -26,16 +20,6 @@ func CreateStreamReader(source []byte) *StreamReader {
return result return result
} }
// GetPosition returns the current stream position
func (v *StreamReader) GetPosition() uint64 {
return v.position
}
// GetSize returns the total size of the stream in bytes
func (v *StreamReader) GetSize() uint64 {
return uint64(len(v.data))
}
// GetByte returns a byte from the stream // GetByte returns a byte from the stream
func (v *StreamReader) GetByte() byte { func (v *StreamReader) GetByte() byte {
result := v.data[v.position] result := v.data[v.position]
@ -44,32 +28,46 @@ func (v *StreamReader) GetByte() byte {
return result return result
} }
// GetUInt16 returns a uint16 word from the stream
func (v *StreamReader) GetUInt16() uint16 {
var result uint16
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint16(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt16
return result
}
// GetInt16 returns a int16 word from the stream // GetInt16 returns a int16 word from the stream
func (v *StreamReader) GetInt16() int16 { func (v *StreamReader) GetInt16() int16 {
var result int16 return int16(v.GetUInt16())
}
for offset := uint64(0); offset < bytesPerInt16; offset++ { // GetUInt16 returns a uint16 word from the stream
shift := uint8(bitsPerByte * offset) //nolint
result += int16(v.data[v.position+offset]) << shift func (v *StreamReader) GetUInt16() uint16 {
} b := v.ReadBytes(2)
return uint16(b[0]) | uint16(b[1])<<8
}
v.position += bytesPerInt16 // GetInt32 returns an int32 dword from the stream
func (v *StreamReader) GetInt32() int32 {
return int32(v.GetUInt32())
}
return result // GetUInt32 returns a uint32 dword from the stream
//nolint
func (v *StreamReader) GetUInt32() uint32 {
b := v.ReadBytes(4)
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
// GetInt64 returns a uint64 qword from the stream
func (v *StreamReader) GetInt64() int64 {
return int64(v.GetUInt64())
}
// GetUInt64 returns a uint64 qword from the stream
//nolint
func (v *StreamReader) GetUInt64() uint64 {
b := v.ReadBytes(8)
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
// GetPosition returns the current stream position
func (v *StreamReader) GetPosition() uint64 {
return v.position
} }
// SetPosition sets the stream position with the given position // SetPosition sets the stream position with the given position
@ -77,51 +75,9 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
v.position = newPosition v.position = newPosition
} }
// GetUInt32 returns a uint32 dword from the stream // GetSize returns the total size of the stream in bytes
func (v *StreamReader) GetUInt32() uint32 { func (v *StreamReader) GetSize() uint64 {
var result uint32 return uint64(len(v.data))
for offset := uint64(0); offset < bytesPerInt32; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint32(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt32
return result
}
// GetInt32 returns an int32 dword from the stream
func (v *StreamReader) GetInt32() int32 {
var result int32
for offset := uint64(0); offset < bytesPerInt32; offset++ {
shift := uint8(bitsPerByte * offset)
result += int32(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt32
return result
}
// GetUint64 returns a uint64 qword from the stream
func (v *StreamReader) GetUint64() uint64 {
var result uint64
for offset := uint64(0); offset < bytesPerInt64; offset++ {
shift := uint8(bitsPerByte * offset)
result += uint64(v.data[v.position+offset]) << shift
}
v.position += bytesPerInt64
return result
}
// GetInt64 returns a uint64 qword from the stream
func (v *StreamReader) GetInt64() int64 {
return int64(v.GetUint64())
} }
// ReadByte implements io.ByteReader // ReadByte implements io.ByteReader

View File

@ -2,10 +2,6 @@ package d2datautils
import "bytes" import "bytes"
const (
byteMask = 0xFF
)
// StreamWriter allows you to create a byte array by streaming in writes of various sizes // StreamWriter allows you to create a byte array by streaming in writes of various sizes
type StreamWriter struct { type StreamWriter struct {
data *bytes.Buffer data *bytes.Buffer
@ -20,41 +16,40 @@ func CreateStreamWriter() *StreamWriter {
return result return result
} }
// GetBytes returns the the byte slice of the underlying data
func (v *StreamWriter) GetBytes() []byte {
return v.data.Bytes()
}
// PushByte writes a byte to the stream // PushByte writes a byte to the stream
func (v *StreamWriter) PushByte(val byte) { func (v *StreamWriter) PushByte(val byte) {
v.data.WriteByte(val) v.data.WriteByte(val)
} }
// PushUint16 writes an uint16 word to the stream
func (v *StreamWriter) PushUint16(val uint16) {
for count := 0; count < bytesPerInt16; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
}
// PushInt16 writes a int16 word to the stream // PushInt16 writes a int16 word to the stream
func (v *StreamWriter) PushInt16(val int16) { func (v *StreamWriter) PushInt16(val int16) {
for count := 0; count < bytesPerInt16; count++ { v.PushUint16(uint16(val))
shift := count * bitsPerByte }
v.data.WriteByte(byte(val>>shift) & byteMask)
} // PushUint16 writes an uint16 word to the stream
//nolint
func (v *StreamWriter) PushUint16(val uint16) {
v.data.WriteByte(byte(val))
v.data.WriteByte(byte(val >> 8))
}
// PushInt32 writes a int32 dword to the stream
func (v *StreamWriter) PushInt32(val int32) {
v.PushUint32(uint32(val))
} }
// PushUint32 writes a uint32 dword to the stream // PushUint32 writes a uint32 dword to the stream
//nolint
func (v *StreamWriter) PushUint32(val uint32) { func (v *StreamWriter) PushUint32(val uint32) {
for count := 0; count < bytesPerInt32; count++ { v.data.WriteByte(byte(val))
shift := count * bitsPerByte v.data.WriteByte(byte(val >> 8))
v.data.WriteByte(byte(val>>shift) & byteMask) v.data.WriteByte(byte(val >> 16))
} v.data.WriteByte(byte(val >> 24))
}
// PushUint64 writes a uint64 qword to the stream
func (v *StreamWriter) PushUint64(val uint64) {
for count := 0; count < bytesPerInt64; count++ {
shift := count * bitsPerByte
v.data.WriteByte(byte(val>>shift) & byteMask)
}
} }
// PushInt64 writes a uint64 qword to the stream // PushInt64 writes a uint64 qword to the stream
@ -62,7 +57,15 @@ func (v *StreamWriter) PushInt64(val int64) {
v.PushUint64(uint64(val)) v.PushUint64(uint64(val))
} }
// GetBytes returns the the byte slice of the underlying data // PushUint64 writes a uint64 qword to the stream
func (v *StreamWriter) GetBytes() []byte { //nolint
return v.data.Bytes() func (v *StreamWriter) PushUint64(val uint64) {
v.data.WriteByte(byte(val))
v.data.WriteByte(byte(val >> 8))
v.data.WriteByte(byte(val >> 16))
v.data.WriteByte(byte(val >> 24))
v.data.WriteByte(byte(val >> 32))
v.data.WriteByte(byte(val >> 40))
v.data.WriteByte(byte(val >> 48))
v.data.WriteByte(byte(val >> 56))
} }

View File

@ -2,7 +2,9 @@ package d2enum
// there are labels for "numeric labels (see AssetManager.TranslateLabel) // there are labels for "numeric labels (see AssetManager.TranslateLabel)
const ( const (
CancelLabel = iota RepairAll = iota
_
CancelLabel
CopyrightLabel CopyrightLabel
AllRightsReservedLabel AllRightsReservedLabel
SinglePlayerLabel SinglePlayerLabel
@ -62,6 +64,8 @@ const (
// BaseLabelNumbers returns base label value (#n in english string table table) // BaseLabelNumbers returns base label value (#n in english string table table)
func BaseLabelNumbers(idx int) int { func BaseLabelNumbers(idx int) int {
baseLabelNumbers := []int{ baseLabelNumbers := []int{
128, // repairAll
127,
// main menu labels // main menu labels
1612, // CANCEL 1612, // CANCEL
1613, // (c) 2000 Blizzard Entertainment 1613, // (c) 2000 Blizzard Entertainment

View File

@ -0,0 +1,11 @@
package d2enum
// SceneState enumerates the different states a scene can be in
type SceneState int
// Scene states
const (
SceneStateUninitialized SceneState = iota
SceneStateBooting
SceneStateBooted
)

View File

@ -0,0 +1,131 @@
package d2mpq
import (
"encoding/binary"
"io"
"strings"
)
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
func cryptoLookup(index uint32) uint32 {
if !cryptoBufferReady {
cryptoInitialize()
cryptoBufferReady = true
}
return cryptoBuffer[index]
}
//nolint:gomnd // Decryption magic
func cryptoInitialize() {
seed := uint32(0x00100001)
for index1 := 0; index1 < 0x100; index1++ {
index2 := index1
for i := 0; i < 5; i++ {
seed = (seed*125 + 3) % 0x2AAAAB
temp1 := (seed & 0xFFFF) << 0x10
seed = (seed*125 + 3) % 0x2AAAAB
temp2 := seed & 0xFFFF
cryptoBuffer[index2] = temp1 | temp2
index2 += 0x100
}
}
}
//nolint:gomnd // Decryption magic
func decrypt(data []uint32, seed uint32) {
seed2 := uint32(0xeeeeeeee)
for i := 0; i < len(data); i++ {
seed2 += cryptoLookup(0x400 + (seed & 0xff))
result := data[i]
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3
data[i] = result
}
}
//nolint:gomnd // Decryption magic
func decryptBytes(data []byte, seed uint32) {
seed2 := uint32(0xEEEEEEEE)
for i := 0; i < len(data)-3; i += 4 {
seed2 += cryptoLookup(0x400 + (seed & 0xFF))
result := binary.LittleEndian.Uint32(data[i : i+4])
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3
data[i+0] = uint8(result & 0xff)
data[i+1] = uint8((result >> 8) & 0xff)
data[i+2] = uint8((result >> 16) & 0xff)
data[i+3] = uint8((result >> 24) & 0xff)
}
}
//nolint:gomnd // Decryption magic
func decryptTable(r io.Reader, size uint32, name string) ([]uint32, error) {
seed := hashString(name, 3)
seed2 := uint32(0xEEEEEEEE)
size *= 4
table := make([]uint32, size)
buf := make([]byte, 4)
for i := uint32(0); i < size; i++ {
seed2 += cryptoBuffer[0x400+(seed&0xff)]
if _, err := r.Read(buf); err != nil {
return table, err
}
result := binary.LittleEndian.Uint32(buf)
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3
table[i] = result
}
return table, nil
}
func hashFilename(key string) uint64 {
a, b := hashString(key, 1), hashString(key, 2)
return uint64(a)<<32 | uint64(b)
}
//nolint:gomnd // Decryption magic
func hashString(key string, hashType uint32) uint32 {
seed1 := uint32(0x7FED7FED)
seed2 := uint32(0xEEEEEEEE)
/* prepare seeds. */
for _, char := range strings.ToUpper(key) {
seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2)
seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3
}
return seed1
}
//nolint:unused,deadcode,gomnd // will use this for creating mpq's
func encrypt(data []uint32, seed uint32) {
seed2 := uint32(0xeeeeeeee)
for i := 0; i < len(data); i++ {
seed2 += cryptoLookup(0x400 + (seed & 0xff))
result := data[i]
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = data[i] + seed2 + (seed2 << 5) + 3
data[i] = result
}
}

View File

@ -1,32 +0,0 @@
package d2mpq
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
func cryptoLookup(index uint32) uint32 {
if !cryptoBufferReady {
cryptoInitialize()
cryptoBufferReady = true
}
return cryptoBuffer[index]
}
//nolint:gomnd // magic cryptographic stuff here...
func cryptoInitialize() {
seed := uint32(0x00100001)
for index1 := 0; index1 < 0x100; index1++ {
index2 := index1
for i := 0; i < 5; i++ {
seed = (seed*125 + 3) % 0x2AAAAB
temp1 := (seed & 0xFFFF) << 0x10
seed = (seed*125 + 3) % 0x2AAAAB
temp2 := seed & 0xFFFF
cryptoBuffer[index2] = temp1 | temp2
index2 += 0x100
}
}
}

View File

@ -1,35 +0,0 @@
package d2mpq
// HashEntryMap represents a hash entry map
type HashEntryMap struct {
entries map[uint64]HashTableEntry
}
// Insert inserts a hash entry into the table
func (hem *HashEntryMap) Insert(entry *HashTableEntry) {
if hem.entries == nil {
hem.entries = make(map[uint64]HashTableEntry)
}
hem.entries[uint64(entry.NamePartA)<<32|uint64(entry.NamePartB)] = *entry
}
// Find finds a hash entry
func (hem *HashEntryMap) Find(fileName string) (*HashTableEntry, bool) {
if hem.entries == nil {
return nil, false
}
hashA := hashString(fileName, 1)
hashB := hashString(fileName, 2)
entry, found := hem.entries[uint64(hashA)<<32|uint64(hashB)]
return &entry, found
}
// Contains returns true if the hash entry contains the values
func (hem *HashEntryMap) Contains(fileName string) bool {
_, found := hem.Find(fileName)
return found
}

View File

@ -2,10 +2,9 @@ package d2mpq
import ( import (
"bufio" "bufio"
"encoding/binary"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -19,33 +18,11 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
// MPQ represents an MPQ archive // MPQ represents an MPQ archive
type MPQ struct { type MPQ struct {
filePath string filePath string
file *os.File file *os.File
hashEntryMap HashEntryMap hashes map[uint64]*Hash
blockTableEntries []BlockTableEntry blocks []*Block
data Data header Header
}
// Data Represents a MPQ file
type Data struct {
Magic [4]byte
HeaderSize uint32
ArchiveSize uint32
FormatVersion uint16
BlockSize uint16
HashTableOffset uint32
BlockTableOffset uint32
HashTableEntries uint32
BlockTableEntries uint32
}
// HashTableEntry represents a hashed file entry in the MPQ file
type HashTableEntry struct { // 16 bytes
NamePartA uint32
NamePartB uint32
Locale uint16
Platform uint16
BlockIndex uint32
} }
// PatchInfo represents patch info for the MPQ. // PatchInfo represents patch info for the MPQ.
@ -53,71 +30,153 @@ type PatchInfo struct {
Length uint32 // Length of patch info header, in bytes Length uint32 // Length of patch info header, in bytes
Flags uint32 // Flags. 0x80000000 = MD5 (?) Flags uint32 // Flags. 0x80000000 = MD5 (?)
DataSize uint32 // Uncompressed size of the patch file DataSize uint32 // Uncompressed size of the patch file
Md5 [16]byte // MD5 of the entire patch file after decompression MD5 [16]byte // MD5 of the entire patch file after decompression
} }
// FileFlag represents flags for a file record in the MPQ archive // New loads an MPQ file and only reads the header
type FileFlag uint32 func New(fileName string) (*MPQ, error) {
mpq := &MPQ{filePath: fileName}
const (
// FileImplode - File is compressed using PKWARE Data compression library
FileImplode FileFlag = 0x00000100
// FileCompress - File is compressed using combination of compression methods
FileCompress FileFlag = 0x00000200
// FileEncrypted - The file is encrypted
FileEncrypted FileFlag = 0x00010000
// FileFixKey - The decryption key for the file is altered according to the position of the file in the archive
FileFixKey FileFlag = 0x00020000
// FilePatchFile - The file contains incremental patch for an existing file in base MPQ
FilePatchFile FileFlag = 0x00100000
// FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit
FileSingleUnit FileFlag = 0x01000000
// FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch
// archives to delete files present in lower-priority archives in the search chain. The file usually
// has length of 0 or 1 byte and its name is a hash
FileDeleteMarker FileFlag = 0x02000000
// FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded.
FileSectorCrc FileFlag = 0x04000000
// FileExists - Set if file exists, reset when the file was deleted
FileExists FileFlag = 0x80000000
)
// BlockTableEntry represents an entry in the block table
type BlockTableEntry struct { // 16 bytes
FilePosition uint32
CompressedFileSize uint32
UncompressedFileSize uint32
Flags FileFlag
// Local Stuff...
FileName string
EncryptionSeed uint32
}
// HasFlag returns true if the specified flag is present
func (v BlockTableEntry) HasFlag(flag FileFlag) bool {
return (v.Flags & flag) != 0
}
// Load loads an MPQ file and returns a MPQ structure
func Load(fileName string) (d2interface.Archive, error) {
result := &MPQ{filePath: fileName}
var err error var err error
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
result.file, err = openIgnoreCase(fileName) mpq.file, err = openIgnoreCase(fileName)
} else { } else {
result.file, err = os.Open(fileName) //nolint:gosec // Will fix later mpq.file, err = os.Open(fileName) //nolint:gosec // Will fix later
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := result.readHeader(); err != nil { if err := mpq.readHeader(); err != nil {
return nil, fmt.Errorf("failed to read reader: %v", err)
}
return mpq, nil
}
// FromFile loads an MPQ file and returns a MPQ structure
func FromFile(fileName string) (*MPQ, error) {
mpq, err := New(fileName)
if err != nil {
return nil, err return nil, err
} }
return result, nil if err := mpq.readHashTable(); err != nil {
return nil, fmt.Errorf("failed to read hash table: %v", err)
}
if err := mpq.readBlockTable(); err != nil {
return nil, fmt.Errorf("failed to read block table: %v", err)
}
return mpq, nil
}
// getFileBlockData gets a block table entry
func (mpq *MPQ) getFileBlockData(fileName string) (*Block, error) {
fileEntry, ok := mpq.hashes[hashFilename(fileName)]
if !ok {
return nil, errors.New("file not found")
}
if fileEntry.BlockIndex >= uint32(len(mpq.blocks)) {
return nil, errors.New("invalid block index")
}
return mpq.blocks[fileEntry.BlockIndex], nil
}
// Close closes the MPQ file
func (mpq *MPQ) Close() error {
return mpq.file.Close()
}
// ReadFile reads a file from the MPQ and returns a memory stream
func (mpq *MPQ) ReadFile(fileName string) ([]byte, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return []byte{}, err
}
fileBlockData.FileName = strings.ToLower(fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return []byte{}, err
}
buffer := make([]byte, fileBlockData.UncompressedFileSize)
if _, err := stream.Read(buffer, 0, fileBlockData.UncompressedFileSize); err != nil {
return []byte{}, err
}
return buffer, nil
}
// ReadFileStream reads the mpq file data and returns a stream
func (mpq *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return nil, err
}
fileBlockData.FileName = strings.ToLower(fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return nil, err
}
return &MpqDataStream{stream: stream}, nil
}
// ReadTextFile reads a file and returns it as a string
func (mpq *MPQ) ReadTextFile(fileName string) (string, error) {
data, err := mpq.ReadFile(fileName)
if err != nil {
return "", err
}
return string(data), nil
}
// Listfile returns the list of files in this MPQ
func (mpq *MPQ) Listfile() ([]string, error) {
data, err := mpq.ReadFile("(listfile)")
if err != nil {
return nil, err
}
raw := strings.TrimRight(string(data), "\x00")
s := bufio.NewScanner(strings.NewReader(raw))
var filePaths []string
for s.Scan() {
filePath := s.Text()
filePaths = append(filePaths, filePath)
}
return filePaths, nil
}
// Path returns the MPQ file path
func (mpq *MPQ) Path() string {
return mpq.filePath
}
// Contains returns bool for whether the given filename exists in the mpq
func (mpq *MPQ) Contains(filename string) bool {
_, ok := mpq.hashes[hashFilename(filename)]
return ok
}
// Size returns the size of the mpq in bytes
func (mpq *MPQ) Size() uint32 {
return mpq.header.ArchiveSize
} }
func openIgnoreCase(mpqPath string) (*os.File, error) { func openIgnoreCase(mpqPath string) (*os.File, error) {
@ -142,258 +201,5 @@ func openIgnoreCase(mpqPath string) (*os.File, error) {
} }
} }
file, err := os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later return os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later
return file, err
}
func (v *MPQ) readHeader() error {
err := binary.Read(v.file, binary.LittleEndian, &v.data)
if err != nil {
return err
}
if string(v.data.Magic[:]) != "MPQ\x1A" {
return errors.New("invalid mpq header")
}
err = v.loadHashTable()
if err != nil {
return err
}
v.loadBlockTable()
return nil
}
func (v *MPQ) loadHashTable() error {
_, err := v.file.Seek(int64(v.data.HashTableOffset), 0)
if err != nil {
log.Panic(err)
}
hashData := make([]uint32, v.data.HashTableEntries*4) //nolint:gomnd // // Decryption magic
hash := make([]byte, 4)
for i := range hashData {
_, err := v.file.Read(hash)
if err != nil {
log.Print(err)
}
hashData[i] = binary.LittleEndian.Uint32(hash)
}
decrypt(hashData, hashString("(hash table)", 3))
for i := uint32(0); i < v.data.HashTableEntries; i++ {
v.hashEntryMap.Insert(&HashTableEntry{
NamePartA: hashData[i*4],
NamePartB: hashData[(i*4)+1],
// https://github.com/OpenDiablo2/OpenDiablo2/issues/812
Locale: uint16(hashData[(i*4)+2] >> 16), //nolint:gomnd // // binary data
Platform: uint16(hashData[(i*4)+2] & 0xFFFF), //nolint:gomnd // // binary data
BlockIndex: hashData[(i*4)+3],
})
}
return nil
}
func (v *MPQ) loadBlockTable() {
_, err := v.file.Seek(int64(v.data.BlockTableOffset), 0)
if err != nil {
log.Panic(err)
}
blockData := make([]uint32, v.data.BlockTableEntries*4) //nolint:gomnd // // binary data
hash := make([]byte, 4)
for i := range blockData {
_, err = v.file.Read(hash) //nolint:errcheck // Will fix later
if err != nil {
log.Print(err)
}
blockData[i] = binary.LittleEndian.Uint32(hash)
}
decrypt(blockData, hashString("(block table)", 3))
for i := uint32(0); i < v.data.BlockTableEntries; i++ {
v.blockTableEntries = append(v.blockTableEntries, BlockTableEntry{
FilePosition: blockData[(i * 4)],
CompressedFileSize: blockData[(i*4)+1],
UncompressedFileSize: blockData[(i*4)+2],
Flags: FileFlag(blockData[(i*4)+3]),
})
}
}
func decrypt(data []uint32, seed uint32) {
seed2 := uint32(0xeeeeeeee) //nolint:gomnd // Decryption magic
for i := 0; i < len(data); i++ {
seed2 += cryptoLookup(0x400 + (seed & 0xff)) //nolint:gomnd // Decryption magic
result := data[i]
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
data[i] = result
}
}
func decryptBytes(data []byte, seed uint32) {
seed2 := uint32(0xEEEEEEEE) //nolint:gomnd // Decryption magic
for i := 0; i < len(data)-3; i += 4 {
seed2 += cryptoLookup(0x400 + (seed & 0xFF)) //nolint:gomnd // Decryption magic
result := binary.LittleEndian.Uint32(data[i : i+4])
result ^= seed + seed2
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
seed2 = result + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
data[i+0] = uint8(result & 0xff) //nolint:gomnd // Decryption magic
data[i+1] = uint8((result >> 8) & 0xff) //nolint:gomnd // Decryption magic
data[i+2] = uint8((result >> 16) & 0xff) //nolint:gomnd // Decryption magic
data[i+3] = uint8((result >> 24) & 0xff) //nolint:gomnd // Decryption magic
}
}
func hashString(key string, hashType uint32) uint32 {
seed1 := uint32(0x7FED7FED) //nolint:gomnd // Decryption magic
seed2 := uint32(0xEEEEEEEE) //nolint:gomnd // Decryption magic
/* prepare seeds. */
for _, char := range strings.ToUpper(key) {
seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2)
seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3 //nolint:gomnd // Decryption magic
}
return seed1
}
// GetFileBlockData gets a block table entry
func (v *MPQ) getFileBlockData(fileName string) (BlockTableEntry, error) {
fileEntry, found := v.hashEntryMap.Find(fileName)
if !found || fileEntry.BlockIndex >= uint32(len(v.blockTableEntries)) {
return BlockTableEntry{}, errors.New("file not found")
}
return v.blockTableEntries[fileEntry.BlockIndex], nil
}
// Close closes the MPQ file
func (v *MPQ) Close() {
err := v.file.Close()
if err != nil {
log.Panic(err)
}
}
// FileExists checks the mpq to see if the file exists
func (v *MPQ) FileExists(fileName string) bool {
return v.hashEntryMap.Contains(fileName)
}
// ReadFile reads a file from the MPQ and returns a memory stream
func (v *MPQ) ReadFile(fileName string) ([]byte, error) {
fileBlockData, err := v.getFileBlockData(fileName)
if err != nil {
return []byte{}, err
}
fileBlockData.FileName = strings.ToLower(fileName)
fileBlockData.calculateEncryptionSeed()
mpqStream, err := CreateStream(v, fileBlockData, fileName)
if err != nil {
return []byte{}, err
}
buffer := make([]byte, fileBlockData.UncompressedFileSize)
mpqStream.Read(buffer, 0, fileBlockData.UncompressedFileSize)
return buffer, nil
}
// ReadFileStream reads the mpq file data and returns a stream
func (v *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := v.getFileBlockData(fileName)
if err != nil {
return nil, err
}
fileBlockData.FileName = strings.ToLower(fileName)
fileBlockData.calculateEncryptionSeed()
mpqStream, err := CreateStream(v, fileBlockData, fileName)
if err != nil {
return nil, err
}
return &MpqDataStream{stream: mpqStream}, nil
}
// ReadTextFile reads a file and returns it as a string
func (v *MPQ) ReadTextFile(fileName string) (string, error) {
data, err := v.ReadFile(fileName)
if err != nil {
return "", err
}
return string(data), nil
}
func (v *BlockTableEntry) calculateEncryptionSeed() {
fileName := path.Base(v.FileName)
v.EncryptionSeed = hashString(fileName, 3)
if !v.HasFlag(FileFixKey) {
return
}
v.EncryptionSeed = (v.EncryptionSeed + v.FilePosition) ^ v.UncompressedFileSize
}
// GetFileList returns the list of files in this MPQ
func (v *MPQ) GetFileList() ([]string, error) {
data, err := v.ReadFile("(listfile)")
if err != nil {
return nil, err
}
raw := strings.TrimRight(string(data), "\x00")
s := bufio.NewScanner(strings.NewReader(raw))
var filePaths []string
for s.Scan() {
filePath := s.Text()
filePaths = append(filePaths, filePath)
}
return filePaths, nil
}
// Path returns the MPQ file path
func (v *MPQ) Path() string {
return v.filePath
}
// Contains returns bool for whether the given filename exists in the mpq
func (v *MPQ) Contains(filename string) bool {
return v.hashEntryMap.Contains(filename)
}
// Size returns the size of the mpq in bytes
func (v *MPQ) Size() uint32 {
return v.data.ArchiveSize
} }

View File

@ -0,0 +1,77 @@
package d2mpq
import (
"io"
"strings"
)
// FileFlag represents flags for a file record in the MPQ archive
type FileFlag uint32
const (
// FileImplode - File is compressed using PKWARE Data compression library
FileImplode FileFlag = 0x00000100
// FileCompress - File is compressed using combination of compression methods
FileCompress FileFlag = 0x00000200
// FileEncrypted - The file is encrypted
FileEncrypted FileFlag = 0x00010000
// FileFixKey - The decryption key for the file is altered according to the position of the file in the archive
FileFixKey FileFlag = 0x00020000
// FilePatchFile - The file contains incremental patch for an existing file in base MPQ
FilePatchFile FileFlag = 0x00100000
// FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit
FileSingleUnit FileFlag = 0x01000000
// FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch
// archives to delete files present in lower-priority archives in the search chain. The file usually
// has length of 0 or 1 byte and its name is a hash
FileDeleteMarker FileFlag = 0x02000000
// FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded.
FileSectorCrc FileFlag = 0x04000000
// FileExists - Set if file exists, reset when the file was deleted
FileExists FileFlag = 0x80000000
)
// Block represents an entry in the block table
type Block struct { // 16 bytes
FilePosition uint32
CompressedFileSize uint32
UncompressedFileSize uint32
Flags FileFlag
// Local Stuff...
FileName string
EncryptionSeed uint32
}
// HasFlag returns true if the specified flag is present
func (b *Block) HasFlag(flag FileFlag) bool {
return (b.Flags & flag) != 0
}
func (b *Block) calculateEncryptionSeed(fileName string) {
fileName = fileName[strings.LastIndex(fileName, `\`)+1:]
seed := hashString(fileName, 3)
b.EncryptionSeed = (seed + b.FilePosition) ^ b.UncompressedFileSize
}
//nolint:gomnd // number
func (mpq *MPQ) readBlockTable() error {
if _, err := mpq.file.Seek(int64(mpq.header.BlockTableOffset), io.SeekStart); err != nil {
return err
}
blockData, err := decryptTable(mpq.file, mpq.header.BlockTableEntries, "(block table)")
if err != nil {
return err
}
for n, i := uint32(0), uint32(0); i < mpq.header.BlockTableEntries; n, i = n+4, i+1 {
mpq.blocks = append(mpq.blocks, &Block{
FilePosition: blockData[n],
CompressedFileSize: blockData[n+1],
UncompressedFileSize: blockData[n+2],
Flags: FileFlag(blockData[n+3]),
})
}
return nil
}

View File

@ -11,14 +11,14 @@ type MpqDataStream struct {
// Read reads data from the data stream // Read reads data from the data stream
func (m *MpqDataStream) Read(p []byte) (n int, err error) { func (m *MpqDataStream) Read(p []byte) (n int, err error) {
totalRead := m.stream.Read(p, 0, uint32(len(p))) totalRead, err := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), nil return int(totalRead), err
} }
// Seek sets the position of the data stream // Seek sets the position of the data stream
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) { func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
m.stream.CurrentPosition = uint32(offset + int64(whence)) m.stream.Position = uint32(offset + int64(whence))
return int64(m.stream.CurrentPosition), nil return int64(m.stream.Position), nil
} }
// Close closes the data stream // Close closes the data stream

View File

@ -0,0 +1,45 @@
package d2mpq
import "io"
// Hash represents a hashed file entry in the MPQ file
type Hash struct { // 16 bytes
A uint32
B uint32
Locale uint16
Platform uint16
BlockIndex uint32
}
// Name64 returns part A and B as uint64
func (h *Hash) Name64() uint64 {
return uint64(h.A)<<32 | uint64(h.B)
}
//nolint:gomnd // number
func (mpq *MPQ) readHashTable() error {
if _, err := mpq.file.Seek(int64(mpq.header.HashTableOffset), io.SeekStart); err != nil {
return err
}
hashData, err := decryptTable(mpq.file, mpq.header.HashTableEntries, "(hash table)")
if err != nil {
return err
}
mpq.hashes = make(map[uint64]*Hash)
for n, i := uint32(0), uint32(0); i < mpq.header.HashTableEntries; n, i = n+4, i+1 {
e := &Hash{
A: hashData[n],
B: hashData[n+1],
// https://github.com/OpenDiablo2/OpenDiablo2/issues/812
Locale: uint16(hashData[n+2] >> 16), //nolint:gomnd // // binary data
Platform: uint16(hashData[n+2] & 0xFFFF), //nolint:gomnd // // binary data
BlockIndex: hashData[n+3],
}
mpq.hashes[e.Name64()] = e
}
return nil
}

View File

@ -0,0 +1,36 @@
package d2mpq
import (
"encoding/binary"
"errors"
"io"
)
// Header Represents a MPQ file
type Header struct {
Magic [4]byte
HeaderSize uint32
ArchiveSize uint32
FormatVersion uint16
BlockSize uint16
HashTableOffset uint32
BlockTableOffset uint32
HashTableEntries uint32
BlockTableEntries uint32
}
func (mpq *MPQ) readHeader() error {
if _, err := mpq.file.Seek(0, io.SeekStart); err != nil {
return err
}
if err := binary.Read(mpq.file, binary.LittleEndian, &mpq.header); err != nil {
return err
}
if string(mpq.header.Magic[:]) != "MPQ\x1A" {
return errors.New("invalid mpq header")
}
return nil
}

View File

@ -6,8 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log" "io"
"strings"
"github.com/JoshVarga/blast" "github.com/JoshVarga/blast"
@ -17,80 +16,63 @@ import (
// Stream represents a stream of data in an MPQ archive // Stream represents a stream of data in an MPQ archive
type Stream struct { type Stream struct {
BlockTableEntry BlockTableEntry Data []byte
BlockPositions []uint32 Positions []uint32
CurrentData []byte MPQ *MPQ
FileName string Block *Block
MPQData *MPQ Index uint32
EncryptionSeed uint32 Size uint32
CurrentPosition uint32 Position uint32
CurrentBlockIndex uint32
BlockSize uint32
} }
// CreateStream creates an MPQ stream // CreateStream creates an MPQ stream
func CreateStream(mpq *MPQ, blockTableEntry BlockTableEntry, fileName string) (*Stream, error) { func CreateStream(mpq *MPQ, block *Block, fileName string) (*Stream, error) {
result := &Stream{ s := &Stream{
MPQData: mpq, MPQ: mpq,
BlockTableEntry: blockTableEntry, Block: block,
CurrentBlockIndex: 0xFFFFFFFF, //nolint:gomnd // MPQ magic Index: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
}
fileSegs := strings.Split(fileName, `\`)
result.EncryptionSeed = hashString(fileSegs[len(fileSegs)-1], 3)
if result.BlockTableEntry.HasFlag(FileFixKey) {
result.EncryptionSeed = (result.EncryptionSeed + result.BlockTableEntry.FilePosition) ^ result.BlockTableEntry.UncompressedFileSize
} }
result.BlockSize = 0x200 << result.MPQData.data.BlockSize //nolint:gomnd // MPQ magic if s.Block.HasFlag(FileFixKey) {
s.Block.calculateEncryptionSeed(fileName)
if result.BlockTableEntry.HasFlag(FilePatchFile) {
log.Fatal("Patching is not supported")
} }
var err error s.Size = 0x200 << s.MPQ.header.BlockSize //nolint:gomnd // MPQ magic
if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) && if s.Block.HasFlag(FilePatchFile) {
!result.BlockTableEntry.HasFlag(FileSingleUnit) { return nil, errors.New("patching is not supported")
err = result.loadBlockOffsets()
} }
return result, err if (s.Block.HasFlag(FileCompress) || s.Block.HasFlag(FileImplode)) && !s.Block.HasFlag(FileSingleUnit) {
if err := s.loadBlockOffsets(); err != nil {
return nil, err
}
}
return s, nil
} }
func (v *Stream) loadBlockOffsets() error { func (v *Stream) loadBlockOffsets() error {
blockPositionCount := ((v.BlockTableEntry.UncompressedFileSize + v.BlockSize - 1) / v.BlockSize) + 1 if _, err := v.MPQ.file.Seek(int64(v.Block.FilePosition), io.SeekStart); err != nil {
v.BlockPositions = make([]uint32, blockPositionCount)
_, err := v.MPQData.file.Seek(int64(v.BlockTableEntry.FilePosition), 0)
if err != nil {
return err return err
} }
mpqBytes := make([]byte, blockPositionCount*4) //nolint:gomnd // MPQ magic blockPositionCount := ((v.Block.UncompressedFileSize + v.Size - 1) / v.Size) + 1
v.Positions = make([]uint32, blockPositionCount)
_, err = v.MPQData.file.Read(mpqBytes) if err := binary.Read(v.MPQ.file, binary.LittleEndian, &v.Positions); err != nil {
if err != nil {
return err return err
} }
for i := range v.BlockPositions { if v.Block.HasFlag(FileEncrypted) {
idx := i * 4 //nolint:gomnd // MPQ magic decrypt(v.Positions, v.Block.EncryptionSeed-1)
v.BlockPositions[i] = binary.LittleEndian.Uint32(mpqBytes[idx : idx+4])
}
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
if v.Positions[0] != blockPosSize {
if v.BlockTableEntry.HasFlag(FileEncrypted) {
decrypt(v.BlockPositions, v.EncryptionSeed-1)
if v.BlockPositions[0] != blockPosSize {
log.Println("Decryption of MPQ failed!")
return errors.New("decryption of MPQ failed") return errors.New("decryption of MPQ failed")
} }
if v.BlockPositions[1] > v.BlockSize+blockPosSize { if v.Positions[1] > v.Size+blockPosSize {
log.Println("Decryption of MPQ failed!")
return errors.New("decryption of MPQ failed") return errors.New("decryption of MPQ failed")
} }
} }
@ -98,16 +80,18 @@ func (v *Stream) loadBlockOffsets() error {
return nil return nil
} }
func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 { func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, err error) {
if v.BlockTableEntry.HasFlag(FileSingleUnit) { if v.Block.HasFlag(FileSingleUnit) {
return v.readInternalSingleUnit(buffer, offset, count) return v.readInternalSingleUnit(buffer, offset, count)
} }
toRead := count var read uint32
readTotal := uint32(0)
toRead := count
for toRead > 0 { for toRead > 0 {
read := v.readInternal(buffer, offset, toRead) if read, err = v.readInternal(buffer, offset, toRead); err != nil {
return 0, err
}
if read == 0 { if read == 0 {
break break
@ -118,149 +102,153 @@ func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
toRead -= read toRead -= read
} }
return readTotal return readTotal, nil
} }
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) uint32 { func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) (uint32, error) {
if len(v.CurrentData) == 0 { if len(v.Data) == 0 {
v.loadSingleUnit() if err := v.loadSingleUnit(); err != nil {
return 0, err
}
} }
bytesToCopy := d2math.Min(uint32(len(v.CurrentData))-v.CurrentPosition, count) return v.copy(buffer, offset, v.Position, count)
copy(buffer[offset:offset+bytesToCopy], v.CurrentData[v.CurrentPosition:v.CurrentPosition+bytesToCopy])
v.CurrentPosition += bytesToCopy
return bytesToCopy
} }
func (v *Stream) readInternal(buffer []byte, offset, count uint32) uint32 { func (v *Stream) readInternal(buffer []byte, offset, count uint32) (uint32, error) {
v.bufferData() if err := v.bufferData(); err != nil {
return 0, err
}
localPosition := v.CurrentPosition % v.BlockSize localPosition := v.Position % v.Size
bytesToCopy := d2math.MinInt32(int32(len(v.CurrentData))-int32(localPosition), int32(count))
return v.copy(buffer, offset, localPosition, count)
}
func (v *Stream) copy(buffer []byte, offset, pos, count uint32) (uint32, error) {
bytesToCopy := d2math.Min(uint32(len(v.Data))-pos, count)
if bytesToCopy <= 0 { if bytesToCopy <= 0 {
return 0 return 0, nil
} }
copy(buffer[offset:offset+uint32(bytesToCopy)], v.CurrentData[localPosition:localPosition+uint32(bytesToCopy)]) copy(buffer[offset:offset+bytesToCopy], v.Data[pos:pos+bytesToCopy])
v.Position += bytesToCopy
v.CurrentPosition += uint32(bytesToCopy) return bytesToCopy, nil
return uint32(bytesToCopy)
} }
func (v *Stream) bufferData() { func (v *Stream) bufferData() (err error) {
requiredBlock := v.CurrentPosition / v.BlockSize blockIndex := v.Position / v.Size
if requiredBlock == v.CurrentBlockIndex { if blockIndex == v.Index {
return return nil
} }
expectedLength := d2math.Min(v.BlockTableEntry.UncompressedFileSize-(requiredBlock*v.BlockSize), v.BlockSize) expectedLength := d2math.Min(v.Block.UncompressedFileSize-(blockIndex*v.Size), v.Size)
v.CurrentData = v.loadBlock(requiredBlock, expectedLength) if v.Data, err = v.loadBlock(blockIndex, expectedLength); err != nil {
v.CurrentBlockIndex = requiredBlock return err
}
v.Index = blockIndex
return nil
} }
func (v *Stream) loadSingleUnit() { func (v *Stream) loadSingleUnit() (err error) {
fileData := make([]byte, v.BlockSize) if _, err = v.MPQ.file.Seek(int64(v.MPQ.header.HeaderSize), io.SeekStart); err != nil {
return err
_, err := v.MPQData.file.Seek(int64(v.MPQData.data.HeaderSize), 0)
if err != nil {
log.Print(err)
} }
_, err = v.MPQData.file.Read(fileData) fileData := make([]byte, v.Size)
if err != nil {
log.Print(err) if _, err = v.MPQ.file.Read(fileData); err != nil {
return err
} }
if v.BlockSize == v.BlockTableEntry.UncompressedFileSize { if v.Size == v.Block.UncompressedFileSize {
v.CurrentData = fileData v.Data = fileData
return return nil
} }
v.CurrentData = decompressMulti(fileData, v.BlockTableEntry.UncompressedFileSize) v.Data, err = decompressMulti(fileData, v.Block.UncompressedFileSize)
return err
} }
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte { func (v *Stream) loadBlock(blockIndex, expectedLength uint32) ([]byte, error) {
var ( var (
offset uint32 offset uint32
toRead uint32 toRead uint32
) )
if v.BlockTableEntry.HasFlag(FileCompress) || v.BlockTableEntry.HasFlag(FileImplode) { if v.Block.HasFlag(FileCompress) || v.Block.HasFlag(FileImplode) {
offset = v.BlockPositions[blockIndex] offset = v.Positions[blockIndex]
toRead = v.BlockPositions[blockIndex+1] - offset toRead = v.Positions[blockIndex+1] - offset
} else { } else {
offset = blockIndex * v.BlockSize offset = blockIndex * v.Size
toRead = expectedLength toRead = expectedLength
} }
offset += v.BlockTableEntry.FilePosition offset += v.Block.FilePosition
data := make([]byte, toRead) data := make([]byte, toRead)
_, err := v.MPQData.file.Seek(int64(offset), 0) if _, err := v.MPQ.file.Seek(int64(offset), io.SeekStart); err != nil {
if err != nil { return []byte{}, err
log.Print(err)
} }
_, err = v.MPQData.file.Read(data) if _, err := v.MPQ.file.Read(data); err != nil {
if err != nil { return []byte{}, err
log.Print(err)
} }
if v.BlockTableEntry.HasFlag(FileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 { if v.Block.HasFlag(FileEncrypted) && v.Block.UncompressedFileSize > 3 {
if v.EncryptionSeed == 0 { if v.Block.EncryptionSeed == 0 {
panic("Unable to determine encryption key") return []byte{}, errors.New("unable to determine encryption key")
} }
decryptBytes(data, blockIndex+v.EncryptionSeed) decryptBytes(data, blockIndex+v.Block.EncryptionSeed)
} }
if v.BlockTableEntry.HasFlag(FileCompress) && (toRead != expectedLength) { if v.Block.HasFlag(FileCompress) && (toRead != expectedLength) {
if !v.BlockTableEntry.HasFlag(FileSingleUnit) { if !v.Block.HasFlag(FileSingleUnit) {
data = decompressMulti(data, expectedLength) return decompressMulti(data, expectedLength)
} else {
data = pkDecompress(data)
} }
return pkDecompress(data)
} }
if v.BlockTableEntry.HasFlag(FileImplode) && (toRead != expectedLength) { if v.Block.HasFlag(FileImplode) && (toRead != expectedLength) {
data = pkDecompress(data) return pkDecompress(data)
} }
return data return data, nil
} }
//nolint:gomnd // Will fix enum values later //nolint:gomnd // Will fix enum values later
func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte { func decompressMulti(data []byte /*expectedLength*/, _ uint32) ([]byte, error) {
compressionType := data[0] compressionType := data[0]
switch compressionType { switch compressionType {
case 1: // Huffman case 1: // Huffman
panic("huffman decompression not supported") return []byte{}, errors.New("huffman decompression not supported")
case 2: // ZLib/Deflate case 2: // ZLib/Deflate
return deflate(data[1:]) return deflate(data[1:])
case 8: // PKLib/Impode case 8: // PKLib/Impode
return pkDecompress(data[1:]) return pkDecompress(data[1:])
case 0x10: // BZip2 case 0x10: // BZip2
panic("bzip2 decompression not supported") return []byte{}, errors.New("bzip2 decompression not supported")
case 0x80: // IMA ADPCM Stereo case 0x80: // IMA ADPCM Stereo
return d2compression.WavDecompress(data[1:], 2) return d2compression.WavDecompress(data[1:], 2), nil
case 0x40: // IMA ADPCM Mono case 0x40: // IMA ADPCM Mono
return d2compression.WavDecompress(data[1:], 1) return d2compression.WavDecompress(data[1:], 1), nil
case 0x12: case 0x12:
panic("lzma decompression not supported") return []byte{}, errors.New("lzma decompression not supported")
// Combos // Combos
case 0x22: case 0x22:
// sparse then zlib // sparse then zlib
panic("sparse decompression + deflate decompression not supported") return []byte{}, errors.New("sparse decompression + deflate decompression not supported")
case 0x30: case 0x30:
// sparse then bzip2 // sparse then bzip2
panic("sparse decompression + bzip2 decompression not supported") return []byte{}, errors.New("sparse decompression + bzip2 decompression not supported")
case 0x41: case 0x41:
sinput := d2compression.HuffmanDecompress(data[1:]) sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 1) sinput = d2compression.WavDecompress(sinput, 1)
@ -268,69 +256,68 @@ func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte {
copy(tmp, sinput) copy(tmp, sinput)
return tmp return tmp, nil
case 0x48: case 0x48:
// byte[] result = PKDecompress(sinput, outputLength); // byte[] result = PKDecompress(sinput, outputLength);
// return MpqWavCompression.Decompress(new MemoryStream(result), 1); // return MpqWavCompression.Decompress(new MemoryStream(result), 1);
panic("pk + mpqwav decompression not supported") return []byte{}, errors.New("pk + mpqwav decompression not supported")
case 0x81: case 0x81:
sinput := d2compression.HuffmanDecompress(data[1:]) sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 2) sinput = d2compression.WavDecompress(sinput, 2)
tmp := make([]byte, len(sinput)) tmp := make([]byte, len(sinput))
copy(tmp, sinput) copy(tmp, sinput)
return tmp return tmp, nil
case 0x88: case 0x88:
// byte[] result = PKDecompress(sinput, outputLength); // byte[] result = PKDecompress(sinput, outputLength);
// return MpqWavCompression.Decompress(new MemoryStream(result), 2); // return MpqWavCompression.Decompress(new MemoryStream(result), 2);
panic("pk + wav decompression not supported") return []byte{}, errors.New("pk + wav decompression not supported")
default:
panic(fmt.Sprintf("decompression not supported for unknown compression type %X", compressionType))
} }
return []byte{}, fmt.Errorf("decompression not supported for unknown compression type %X", compressionType)
} }
func deflate(data []byte) []byte { func deflate(data []byte) ([]byte, error) {
b := bytes.NewReader(data) b := bytes.NewReader(data)
r, err := zlib.NewReader(b) r, err := zlib.NewReader(b)
if err != nil { if err != nil {
panic(err) return []byte{}, err
} }
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(r) _, err = buffer.ReadFrom(r)
if err != nil { if err != nil {
log.Panic(err) return []byte{}, err
} }
err = r.Close() err = r.Close()
if err != nil { if err != nil {
log.Panic(err) return []byte{}, err
} }
return buffer.Bytes() return buffer.Bytes(), nil
} }
func pkDecompress(data []byte) []byte { func pkDecompress(data []byte) ([]byte, error) {
b := bytes.NewReader(data) b := bytes.NewReader(data)
r, err := blast.NewReader(b)
r, err := blast.NewReader(b)
if err != nil { if err != nil {
panic(err) return []byte{}, err
} }
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(r) if _, err = buffer.ReadFrom(r); err != nil {
if err != nil { return []byte{}, err
panic(err)
} }
err = r.Close() err = r.Close()
if err != nil { if err != nil {
panic(err) return []byte{}, err
} }
return buffer.Bytes() return buffer.Bytes(), nil
} }

View File

@ -8,10 +8,9 @@ type Archive interface {
Path() string Path() string
Contains(string) bool Contains(string) bool
Size() uint32 Size() uint32
Close() Close() error
FileExists(fileName string) bool
ReadFile(fileName string) ([]byte, error) ReadFile(fileName string) ([]byte, error)
ReadFileStream(fileName string) (DataStream, error) ReadFileStream(fileName string) (DataStream, error)
ReadTextFile(fileName string) (string, error) ReadTextFile(fileName string) (string, error)
GetFileList() ([]string, error) Listfile() ([]string, error)
} }

View File

@ -1,7 +1,5 @@
package d2interface package d2interface
import "github.com/hajimehoshi/ebiten/v2"
type renderCallback = func(Surface) error type renderCallback = func(Surface) error
type updateCallback = func() error type updateCallback = func() error
@ -21,7 +19,7 @@ type Renderer interface {
GetCursorPos() (int, int) GetCursorPos() (int, int)
CurrentFPS() float64 CurrentFPS() float64
ShowPanicScreen(message string) ShowPanicScreen(message string)
Print(target *ebiten.Image, str string) error Print(target interface{}, str string) error
PrintAt(target *ebiten.Image, str string, x, y int) PrintAt(target interface{}, str string, x, y int)
GetWindowSize() (int, int) GetWindowSize() (int, int)
} }

View File

@ -2,11 +2,14 @@ package d2interface
import ( import (
"github.com/gravestench/akara" "github.com/gravestench/akara"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
) )
// Scene is an extension of akara.System // Scene is an extension of akara.System
type Scene interface { type Scene interface {
akara.SystemInitializer akara.SystemInitializer
State() d2enum.SceneState
Key() string Key() string
Booted() bool Booted() bool
Paused() bool Paused() bool

View File

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

View File

@ -37,7 +37,8 @@ func Ext2SourceType(ext string) SourceType {
func CheckSourceType(path string) SourceType { func CheckSourceType(path string) SourceType {
// on MacOS, the MPQ's from blizzard don't have file extensions // on MacOS, the MPQ's from blizzard don't have file extensions
// so we just attempt to init the file as an mpq // so we just attempt to init the file as an mpq
if _, err := d2mpq.Load(path); err == nil { if mpq, err := d2mpq.New(path); err == nil {
_ = mpq.Close()
return AssetSourceMPQ return AssetSourceMPQ
} }

View File

@ -14,7 +14,7 @@ var _ asset.Source = &Source{}
// NewSource creates a new MPQ Source // NewSource creates a new MPQ Source
func NewSource(sourcePath string) (asset.Source, error) { func NewSource(sourcePath string) (asset.Source, error) {
loaded, err := d2mpq.Load(sourcePath) loaded, err := d2mpq.FromFile(sourcePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -62,7 +62,7 @@ key | value key | value
So, GetLabelModifier returns value of offset in locale languages table So, GetLabelModifier returns value of offset in locale languages table
*/ */
// some of values need to be set up. For now values with "checked" comment // some of values need to be set up. For now values with "checked" comment
// was tested and works fine in main menu. // was tested and works fine.
func GetLabelModifier(language string) int { func GetLabelModifier(language string) int {
modifiers := map[string]int{ modifiers := map[string]int{
"ENG": 0, // (English) // checked "ENG": 0, // (English) // checked
@ -70,7 +70,7 @@ func GetLabelModifier(language string) int {
"DEU": 0, // (German) // checked "DEU": 0, // (German) // checked
"FRA": 0, // (French) "FRA": 0, // (French)
"POR": 0, // (Portuguese) "POR": 0, // (Portuguese)
"ITA": 0, // (Italian) "ITA": 0, // (Italian) // checked
"JPN": 0, // (Japanese) "JPN": 0, // (Japanese)
"KOR": 0, // (Korean) "KOR": 0, // (Korean)
"SIN": 0, // "SIN": 0, //

View File

@ -193,6 +193,7 @@ const (
QuestLogQDescrBtn = "/data/global/ui/MENU/questlast.dc6" QuestLogQDescrBtn = "/data/global/ui/MENU/questlast.dc6"
QuestLogSocket = "/data/global/ui/MENU/questsockets.dc6" QuestLogSocket = "/data/global/ui/MENU/questsockets.dc6"
QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6" QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6"
QuestLogDoneSfx = "cursor/questdone.wav"
// --- Mouse Pointers --- // --- Mouse Pointers ---
@ -243,16 +244,18 @@ const (
MinipanelSmall = "/data/global/ui/PANEL/minipanel_s.dc6" MinipanelSmall = "/data/global/ui/PANEL/minipanel_s.dc6"
MinipanelButton = "/data/global/ui/PANEL/minipanelbtn.DC6" MinipanelButton = "/data/global/ui/PANEL/minipanelbtn.DC6"
Frame = "/data/global/ui/PANEL/800borderframe.dc6" Frame = "/data/global/ui/PANEL/800borderframe.dc6"
InventoryCharacterPanel = "/data/global/ui/PANEL/invchar6.DC6" InventoryCharacterPanel = "/data/global/ui/PANEL/invchar6.DC6"
InventoryWeaponsTab = "/data/global/ui/PANEL/invchar6Tab.DC6" HeroStatsPanelStatsPoints = "/data/global/ui/PANEL/skillpoints.dc6"
SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_back.DC6" HeroStatsPanelSocket = "/data/global/ui/PANEL/levelsocket.dc6"
SkillsPanelBarbarian = "/data/global/ui/SPELLS/skltree_b_back.DC6" InventoryWeaponsTab = "/data/global/ui/PANEL/invchar6Tab.DC6"
SkillsPanelDruid = "/data/global/ui/SPELLS/skltree_d_back.DC6" SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_back.DC6"
SkillsPanelAssassin = "/data/global/ui/SPELLS/skltree_i_back.DC6" SkillsPanelBarbarian = "/data/global/ui/SPELLS/skltree_b_back.DC6"
SkillsPanelNecromancer = "/data/global/ui/SPELLS/skltree_n_back.DC6" SkillsPanelDruid = "/data/global/ui/SPELLS/skltree_d_back.DC6"
SkillsPanelPaladin = "/data/global/ui/SPELLS/skltree_p_back.DC6" SkillsPanelAssassin = "/data/global/ui/SPELLS/skltree_i_back.DC6"
SkillsPanelSorcerer = "/data/global/ui/SPELLS/skltree_s_back.DC6" SkillsPanelNecromancer = "/data/global/ui/SPELLS/skltree_n_back.DC6"
SkillsPanelPaladin = "/data/global/ui/SPELLS/skltree_p_back.DC6"
SkillsPanelSorcerer = "/data/global/ui/SPELLS/skltree_s_back.DC6"
GenericSkills = "/data/global/ui/SPELLS/Skillicon.DC6" GenericSkills = "/data/global/ui/SPELLS/Skillicon.DC6"
AmazonSkills = "/data/global/ui/SPELLS/AmSkillicon.DC6" AmazonSkills = "/data/global/ui/SPELLS/AmSkillicon.DC6"

View File

@ -332,7 +332,7 @@ func (a *Sprite) GetDirection() int {
// SetCurrentFrame sets sprite at a specific frame // SetCurrentFrame sets sprite at a specific frame
func (a *Sprite) SetCurrentFrame(frameIndex int) error { func (a *Sprite) SetCurrentFrame(frameIndex int) error {
if frameIndex >= a.GetFrameCount() { if frameIndex >= a.GetFrameCount() || frameIndex < 0 {
return errors.New("invalid frame index") return errors.New("invalid frame index")
} }

View File

@ -37,16 +37,16 @@ type GlyphPrinter struct {
// Basic Latin and C1 Controls and Latin-1 Supplement. // Basic Latin and C1 Controls and Latin-1 Supplement.
// //
// DebugPrint always returns nil as of 1.5.0-alpha. // DebugPrint always returns nil as of 1.5.0-alpha.
func (p *GlyphPrinter) Print(target *ebiten.Image, str string) error { func (p *GlyphPrinter) Print(target interface{}, str string) error {
p.PrintAt(target, str, 0, 0) p.PrintAt(target.(*ebiten.Image), str, 0, 0)
return nil return nil
} }
// PrintAt draws the string str on the image at (x, y) position. // PrintAt draws the string str on the image at (x, y) position.
// The available runes are in U+0000 to U+00FF, which is C0 Controls and // The available runes are in U+0000 to U+00FF, which is C0 Controls and
// Basic Latin and C1 Controls and Latin-1 Supplement. // Basic Latin and C1 Controls and Latin-1 Supplement.
func (p *GlyphPrinter) PrintAt(target *ebiten.Image, str string, x, y int) { func (p *GlyphPrinter) PrintAt(target interface{}, str string, x, y int) {
p.drawDebugText(target, str, x, y, false) p.drawDebugText(target.(*ebiten.Image), str, x, y, false)
} }
func (p *GlyphPrinter) drawDebugText(target *ebiten.Image, str string, ox, oy int, shadow bool) { func (p *GlyphPrinter) drawDebugText(target *ebiten.Image, str string, ox, oy int, shadow bool) {

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"os" "os"
"runtime" "runtime"
"sync"
) )
// LogLevel determines how verbose the logging is (higher is more verbose) // LogLevel determines how verbose the logging is (higher is more verbose)
@ -51,6 +52,7 @@ func NewLogger() *Logger {
l := &Logger{ l := &Logger{
level: LogLevelDefault, level: LogLevelDefault,
colorEnabled: true, colorEnabled: true,
mutex: sync.Mutex{},
} }
l.Writer = log.Writer() l.Writer = log.Writer()
@ -64,6 +66,7 @@ type Logger struct {
io.Writer io.Writer
level LogLevel level LogLevel
colorEnabled bool colorEnabled bool
mutex sync.Mutex
} }
// SetPrefix sets a prefix for the message. // SetPrefix sets a prefix for the message.
@ -71,11 +74,17 @@ type Logger struct {
// logger.SetPrefix("XYZ") // logger.SetPrefix("XYZ")
// logger.Debug("ABC") will print "[XYZ] [DEBUG] ABC" // logger.Debug("ABC") will print "[XYZ] [DEBUG] ABC"
func (l *Logger) SetPrefix(s string) { func (l *Logger) SetPrefix(s string) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.prefix = s l.prefix = s
} }
// SetLevel sets the log level // SetLevel sets the log level
func (l *Logger) SetLevel(level LogLevel) { func (l *Logger) SetLevel(level LogLevel) {
l.mutex.Lock()
defer l.mutex.Unlock()
if level == LogLevelUnspecified { if level == LogLevelUnspecified {
level = LogLevelDefault level = LogLevelDefault
} }
@ -85,6 +94,9 @@ func (l *Logger) SetLevel(level LogLevel) {
// SetColorEnabled adds color escape-sequences to the logging output // SetColorEnabled adds color escape-sequences to the logging output
func (l *Logger) SetColorEnabled(b bool) { func (l *Logger) SetColorEnabled(b bool) {
l.mutex.Lock()
defer l.mutex.Unlock()
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
b = false b = false
} }
@ -94,10 +106,6 @@ func (l *Logger) SetColorEnabled(b bool) {
// Info logs an info message // Info logs an info message
func (l *Logger) Info(msg string) { func (l *Logger) Info(msg string) {
if l == nil || l.level < LogLevelInfo {
return
}
go l.print(LogLevelInfo, msg) go l.print(LogLevelInfo, msg)
} }
@ -108,10 +116,6 @@ func (l *Logger) Infof(fmtMsg string, args ...interface{}) {
// Warning logs a warning message // Warning logs a warning message
func (l *Logger) Warning(msg string) { func (l *Logger) Warning(msg string) {
if l == nil || l.level < LogLevelWarning {
return
}
go l.print(LogLevelWarning, msg) go l.print(LogLevelWarning, msg)
} }
@ -122,10 +126,6 @@ func (l *Logger) Warningf(fmtMsg string, args ...interface{}) {
// Error logs an error message // Error logs an error message
func (l *Logger) Error(msg string) { func (l *Logger) Error(msg string) {
if l == nil || l.level < LogLevelError {
return
}
go l.print(LogLevelError, msg) go l.print(LogLevelError, msg)
} }
@ -136,10 +136,6 @@ func (l *Logger) Errorf(fmtMsg string, args ...interface{}) {
// Fatal logs an fatal error message and exits programm // Fatal logs an fatal error message and exits programm
func (l *Logger) Fatal(msg string) { func (l *Logger) Fatal(msg string) {
if l == nil || l.level < LogLevelFatal {
return
}
go l.print(LogLevelFatal, msg) go l.print(LogLevelFatal, msg)
os.Exit(1) os.Exit(1)
} }
@ -151,10 +147,6 @@ func (l *Logger) Fatalf(fmtMsg string, args ...interface{}) {
// Debug logs a debug message // Debug logs a debug message
func (l *Logger) Debug(msg string) { func (l *Logger) Debug(msg string) {
if l == nil || l.level < LogLevelDebug {
return
}
go l.print(LogLevelDebug, msg) go l.print(LogLevelDebug, msg)
} }
@ -164,7 +156,10 @@ func (l *Logger) Debugf(fmtMsg string, args ...interface{}) {
} }
func (l *Logger) print(level LogLevel, msg string) { func (l *Logger) print(level LogLevel, msg string) {
if l == nil || l.level < level { l.mutex.Lock()
defer l.mutex.Unlock()
if l.level < level {
return return
} }

View File

@ -323,7 +323,7 @@ func (a *Animation) GetDirection() int {
// SetCurrentFrame sets animation at a specific frame // SetCurrentFrame sets animation at a specific frame
func (a *Animation) SetCurrentFrame(frameIndex int) error { func (a *Animation) SetCurrentFrame(frameIndex int) error {
if frameIndex >= a.GetFrameCount() { if frameIndex >= a.GetFrameCount() || frameIndex < 0 {
return errors.New("invalid frame index") return errors.New("invalid frame index")
} }

View File

@ -3,6 +3,7 @@ package d2asset
import ( import (
"fmt" "fmt"
"image/color" "image/color"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "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. // BindTerminalCommands binds the in-game terminal comands for the asset manager.
func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error { 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 { if verbose {
term.OutputInfof("asset manager verbose logging enabled") term.Infof("asset manager verbose logging enabled")
} else { } else {
term.OutputInfof("asset manager verbose logging disabled") term.Infof("asset manager verbose logging disabled")
} }
am.palettes.SetVerbose(verbose) am.palettes.SetVerbose(verbose)
am.fonts.SetVerbose(verbose) am.fonts.SetVerbose(verbose)
am.transforms.SetVerbose(verbose) am.transforms.SetVerbose(verbose)
am.animations.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 { var cacheStatistics = func(c d2interface.Cache) float64 {
const percent = 100.0 const percent = 100.0
return float64(c.GetWeight()) / float64(c.GetBudget()) * percent return float64(c.GetWeight()) / float64(c.GetBudget()) * percent
} }
term.OutputInfof("palette cache: %f", cacheStatistics(am.palettes)) term.Infof("palette cache: %f", cacheStatistics(am.palettes))
term.OutputInfof("palette transform cache: %f", cacheStatistics(am.transforms)) term.Infof("palette transform cache: %f", cacheStatistics(am.transforms))
term.OutputInfof("Animation cache: %f", cacheStatistics(am.animations)) term.Infof("Animation cache: %f", cacheStatistics(am.animations))
term.OutputInfof("font cache: %f", cacheStatistics(am.fonts)) term.Infof("font cache: %f", cacheStatistics(am.fonts))
}); err != nil {
return err
}
if err := term.BindAction("assetclear", "clear asset manager cache", func() { return nil
am.palettes.Clear()
am.transforms.Clear()
am.animations.Clear()
am.fonts.Clear()
}); err != nil {
return err
} }
}
func (am *AssetManager) commandAssetClear([]string) error {
am.palettes.Clear()
am.transforms.Clear()
am.animations.Clear()
am.fonts.Clear()
return nil return nil
} }

View File

@ -9,19 +9,23 @@ import (
) )
// NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly // NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly
func NewAssetManager() (*AssetManager, error) { func NewAssetManager(logLevel d2util.LogLevel) (*AssetManager, error) {
loader, err := d2loader.NewLoader(d2util.LogLevelDefault) loader, err := d2loader.NewLoader(logLevel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
records, err := d2records.NewRecordManager(d2util.LogLevelDebug) records, err := d2records.NewRecordManager(logLevel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
logger := d2util.NewLogger()
logger.SetPrefix(logPrefix)
logger.SetLevel(logLevel)
manager := &AssetManager{ manager := &AssetManager{
Logger: d2util.NewLogger(), Logger: logger,
Loader: loader, Loader: loader,
tables: make([]d2tbl.TextDictionary, 0), tables: make([]d2tbl.TextDictionary, 0),
animations: d2cache.CreateCache(animationBudget), animations: d2cache.CreateCache(animationBudget),
@ -31,7 +35,5 @@ func NewAssetManager() (*AssetManager, error) {
Records: records, Records: records,
} }
manager.SetPrefix(logPrefix)
return manager, err return manager, err
} }

View File

@ -2,9 +2,6 @@ package d2asset
import ( import (
"errors" "errors"
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
@ -132,24 +129,11 @@ func (a *DCCAnimation) decodeDirection(directionIndex int) error {
func (a *DCCAnimation) decodeFrame(directionIndex int) animationFrame { func (a *DCCAnimation) decodeFrame(directionIndex int) animationFrame {
dccDirection := a.dcc.Directions[directionIndex] dccDirection := a.dcc.Directions[directionIndex]
minX, minY := math.MaxInt32, math.MaxInt32
maxX, maxY := math.MinInt32, math.MinInt32
for _, dccFrame := range dccDirection.Frames {
minX = d2math.MinInt(minX, dccFrame.Box.Left)
minY = d2math.MinInt(minY, dccFrame.Box.Top)
maxX = d2math.MaxInt(maxX, dccFrame.Box.Right())
maxY = d2math.MaxInt(maxY, dccFrame.Box.Bottom())
}
frameWidth := maxX - minX
frameHeight := maxY - minY
frame := animationFrame{ frame := animationFrame{
width: frameWidth, width: dccDirection.Box.Width,
height: frameHeight, height: dccDirection.Box.Height,
offsetX: minX, offsetX: dccDirection.Box.Left,
offsetY: minY, offsetY: dccDirection.Box.Top,
decoded: true, decoded: true,
} }

View File

@ -3,6 +3,7 @@ package d2audio
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
@ -31,7 +32,7 @@ const originalFPS float64 = 25
// A Sound that can be started and stopped // A Sound that can be started and stopped
type Sound struct { type Sound struct {
effect d2interface.SoundEffect effect d2interface.SoundEffect
entry *d2records.SoundDetailsRecord entry *d2records.SoundDetailRecord
volume float64 volume float64
vTarget float64 vTarget float64
vRate float64 vRate float64
@ -73,7 +74,7 @@ func (s *Sound) SetPan(pan float64) {
// Play the sound // Play the sound
func (s *Sound) Play() { func (s *Sound) Play() {
s.Info("starting sound" + s.entry.Handle) s.Info("starting sound " + s.entry.Handle)
s.effect.Play() s.effect.Play()
if s.entry.FadeIn != 0 { 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 // SoundEngine provides functions for playing sounds
type SoundEngine struct { type SoundEngine struct {
asset *d2asset.AssetManager asset *d2asset.AssetManager
@ -128,43 +134,25 @@ func NewSoundEngine(provider d2interface.AudioProvider,
r.Logger.SetPrefix(logPrefix) r.Logger.SetPrefix(logPrefix)
r.Logger.SetLevel(l) r.Logger.SetLevel(l)
err := term.BindAction("playsoundid", "plays the sound for a given id", func(id int) { if err := term.Bind("playsoundid", "plays the sound for a given id", []string{"id"}, r.commandPlaySoundID); err != nil {
r.PlaySoundID(id)
})
if err != nil {
r.Error(err.Error()) r.Error(err.Error())
return nil return nil
} }
err = term.BindAction("playsound", "plays the sound for a given handle string", func(handle string) { if err := term.Bind("playsound", "plays the sound for a given handle string", []string{"name"}, r.commandPlaySound); err != nil {
r.PlaySoundHandle(handle)
})
if err != nil {
r.Error(err.Error()) r.Error(err.Error())
return nil return nil
} }
err = term.BindAction("activesounds", "list currently active sounds", func() { if err := term.Bind("activesounds", "list currently active sounds", nil, r.commandActiveSounds); err != nil {
for s := range r.sounds { r.Error(err.Error())
if err != nil { return nil
r.Error(err.Error()) }
return
}
r.Info(fmt.Sprint(s)) if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil {
} r.Error(err.Error())
}) return nil
}
err = term.BindAction("killsounds", "kill active sounds", func() {
for s := range r.sounds {
if err != nil {
r.Error(err.Error())
return
}
s.Stop()
}
})
return &r 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 // Reset stop all sounds and reset state
func (s *SoundEngine) Reset() { func (s *SoundEngine) Reset() {
for snd := range s.sounds { for snd := range s.sounds {
@ -242,3 +235,35 @@ func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
sound := s.asset.Records.Sound.Details[handle].Index sound := s.asset.Records.Sound.Details[handle].Index
return s.PlaySoundID(sound) 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

@ -13,7 +13,8 @@ type CommandRegistration struct {
Enabled bool Enabled bool
Name string Name string
Description string Description string
Callback interface{} Args []string
Callback func(args []string) error
} }
// New creates a new CommandRegistration. By default, IsCommandRegistration is false. // New creates a new CommandRegistration. By default, IsCommandRegistration is false.

View File

@ -5,8 +5,6 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
) )
// Configuration defines the configuration for the engine, loaded from config.json // Configuration defines the configuration for the engine, loaded from config.json
@ -21,7 +19,6 @@ type Configuration struct {
RunInBackground bool RunInBackground bool
VsyncEnabled bool VsyncEnabled bool
Backend string Backend string
LogLevel d2util.LogLevel
path string path string
} }

View File

@ -4,8 +4,6 @@ import (
"os/user" "os/user"
"path" "path"
"runtime" "runtime"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
) )
// DefaultConfig creates and returns a default configuration // DefaultConfig creates and returns a default configuration
@ -37,8 +35,7 @@ func DefaultConfig() *Configuration {
"d2video.mpq", "d2video.mpq",
"d2speech.mpq", "d2speech.mpq",
}, },
LogLevel: d2util.LogLevelDefault, path: DefaultConfigPath(),
path: DefaultConfigPath(),
} }
switch runtime.GOOS { switch runtime.GOOS {

View File

@ -74,8 +74,7 @@ func NewBox(
renderer d2interface.Renderer, renderer d2interface.Renderer,
ui *d2ui.UIManager, ui *d2ui.UIManager,
contentLayout *Layout, contentLayout *Layout,
width, height int, width, height, x, y int,
x, y int,
l d2util.LogLevel, l d2util.LogLevel,
title string, title string,
) *Box { ) *Box {

View File

@ -1,8 +1,6 @@
package d2gui package d2gui
import ( import (
"image/color"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
) )
@ -37,28 +35,3 @@ func renderSegmented(animation d2interface.Animation, segmentsX, segmentsY, fram
func half(n int) int { func half(n int) int {
return n / 2 return n / 2
} }
func rgbaColor(rgba uint32) color.RGBA {
result := color.RGBA{}
a, b, g, r := 0, 1, 2, 3
byteWidth := 8
byteMask := 0xff
for idx := 0; idx < 4; idx++ {
shift := idx * byteWidth
component := uint8(rgba>>shift) & uint8(byteMask)
switch idx {
case a:
result.A = component
case b:
result.B = component
case g:
result.G = component
case r:
result.R = component
}
}
return result
}

View File

@ -248,16 +248,16 @@ func (l *Layout) renderEntryDebug(entry *layoutEntry, target d2interface.Surface
target.PushTranslation(entry.x, entry.y) target.PushTranslation(entry.x, entry.y)
defer target.Pop() defer target.Pop()
drawColor := rgbaColor(white) drawColor := d2util.Color(white)
switch entry.widget.(type) { switch entry.widget.(type) {
case *Layout: case *Layout:
drawColor = rgbaColor(magenta) drawColor = d2util.Color(magenta)
case *SpacerStatic, *SpacerDynamic: case *SpacerStatic, *SpacerDynamic:
drawColor = rgbaColor(grey2) drawColor = d2util.Color(grey2)
case *Label: case *Label:
drawColor = rgbaColor(green) drawColor = d2util.Color(green)
case *Button: case *Button:
drawColor = rgbaColor(yellow) drawColor = d2util.Color(yellow)
} }
target.DrawLine(entry.width, 0, drawColor) target.DrawLine(entry.width, 0, drawColor)
@ -487,7 +487,7 @@ func (l *Layout) createButton(renderer d2interface.Renderer, text string,
return nil, loadErr return nil, loadErr
} }
textColor := rgbaColor(grey) textColor := d2util.Color(grey)
textWidth, textHeight := font.GetTextMetrics(text) textWidth, textHeight := font.GetTextMetrics(text)
textX := half(buttonWidth) - half(textWidth) textX := half(buttonWidth) - half(textWidth)
textY := half(buttonHeight) - half(textHeight) + config.textOffset textY := half(buttonHeight) - half(textHeight) + config.textOffset

View File

@ -64,10 +64,7 @@ const (
) )
// NewLayoutScrollbar attaches a scrollbar to the parentLayout to control the targetLayout // NewLayoutScrollbar attaches a scrollbar to the parentLayout to control the targetLayout
func NewLayoutScrollbar( func NewLayoutScrollbar(parentLayout, targetLayout *Layout) *LayoutScrollbar {
parentLayout *Layout,
targetLayout *Layout,
) *LayoutScrollbar {
parentW, parentH := parentLayout.GetSize() parentW, parentH := parentLayout.GetSize()
_, targetH := targetLayout.GetSize() _, targetH := targetLayout.GetSize()
gutterHeight := parentH - (2 * textSliderPartHeight) gutterHeight := parentH - (2 * textSliderPartHeight)

View File

@ -110,7 +110,7 @@ func (f *HeroStateFactory) GetAllHeroStates() ([]*HeroState, error) {
} }
// CreateHeroSkillsState will assemble the hero skills from the class stats record. // CreateHeroSkillsState will assemble the hero skills from the class stats record.
func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatsRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) { func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) {
baseSkills := map[int]*HeroSkill{} baseSkills := map[int]*HeroSkill{}
for idx := range classStats.BaseSkill { for idx := range classStats.BaseSkill {

View File

@ -10,32 +10,27 @@ type HeroStatsState struct {
Level int `json:"level"` Level int `json:"level"`
Experience int `json:"experience"` Experience int `json:"experience"`
Vitality int `json:"vitality"`
Energy int `json:"energy"`
Strength int `json:"strength"` Strength int `json:"strength"`
Energy int `json:"energy"`
Dexterity int `json:"dexterity"` Dexterity int `json:"dexterity"`
Vitality int `json:"vitality"`
// there are stats and skills points remaining to add.
StatsPoints int `json:"statsPoints"`
SkillPoints int `json:"skillPoints"`
AttackRating int `json:"attackRating"` Health int `json:"health"`
DefenseRating int `json:"defenseRating"` MaxHealth int `json:"maxHealth"`
Mana int `json:"mana"`
MaxStamina int `json:"maxStamina"` MaxMana int `json:"maxMana"`
Health int `json:"health"` Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
MaxHealth int `json:"maxHealth"` MaxStamina int `json:"maxStamina"`
Mana int `json:"mana"`
MaxMana int `json:"maxMana"`
FireResistance int `json:"fireResistance"`
ColdResistance int `json:"coldResistance"`
LightningResistance int `json:"lightningResistance"`
PoisonResistance int `json:"poisonResistance"`
// values which are not saved/loaded(computed) // values which are not saved/loaded(computed)
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world NextLevelExp int `json:"-"`
NextLevelExp int `json:"-"`
} }
// CreateHeroStatsState generates a running state from a hero stats. // CreateHeroStatsState generates a running state from a hero stats.
func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2records.CharStatsRecord) *HeroStatsState { func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2records.CharStatRecord) *HeroStatsState {
result := HeroStatsState{ result := HeroStatsState{
Level: 1, Level: 1,
Experience: 0, Experience: 0,
@ -44,6 +39,8 @@ func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStat
Dexterity: classStats.InitDex, Dexterity: classStats.InitDex,
Vitality: classStats.InitVit, Vitality: classStats.InitVit,
Energy: classStats.InitEne, Energy: classStats.InitEne,
StatsPoints: 0,
SkillPoints: 0,
MaxHealth: classStats.InitVit * classStats.LifePerVit, MaxHealth: classStats.InitVit * classStats.LifePerVit,
MaxMana: classStats.InitEne * classStats.ManaPerEne, MaxMana: classStats.InitEne * classStats.ManaPerEne,

View File

@ -136,7 +136,6 @@ func (f *InventoryItemFactory) GetMiscItemByCode(code string) (*InventoryItemMis
// GetWeaponItemByCode returns the weapon item for the given code // GetWeaponItemByCode returns the weapon item for the given code
func (f *InventoryItemFactory) GetWeaponItemByCode(code string) (*InventoryItemWeapon, error) { func (f *InventoryItemFactory) GetWeaponItemByCode(code string) (*InventoryItemWeapon, error) {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/796
result := f.asset.Records.Item.Weapons[code] result := f.asset.Records.Item.Weapons[code]
if result == nil { if result == nil {
return nil, fmt.Errorf("could not find weapon entry for code '%s'", code) return nil, fmt.Errorf("could not find weapon entry for code '%s'", code)

View File

@ -277,7 +277,7 @@ var itemStatCosts = map[string]*d2records.ItemStatCostRecord{
} }
// nolint:gochecknoglobals // just a test // nolint:gochecknoglobals // just a test
var charStats = map[d2enum.Hero]*d2records.CharStatsRecord{ var charStats = map[d2enum.Hero]*d2records.CharStatRecord{
d2enum.HeroPaladin: { d2enum.HeroPaladin: {
Class: d2enum.HeroPaladin, Class: d2enum.HeroPaladin,
SkillStrAll: "to Paladin Skill Levels", SkillStrAll: "to Paladin Skill Levels",
@ -297,7 +297,7 @@ var skillDetails = map[int]*d2records.SkillRecord{
} }
// nolint:gochecknoglobals // just a test // nolint:gochecknoglobals // just a test
var monStats = map[string]*d2records.MonStatsRecord{ var monStats = map[string]*d2records.MonStatRecord{
"Specter": {NameString: "Specter", ID: 40}, "Specter": {NameString: "Specter", ID: 40},
} }

View File

@ -256,7 +256,7 @@ func (m *MapEngine) RemoveEntity(entity d2interface.MapEntity) {
// GetTiles returns a slice of all tiles matching the given style, // GetTiles returns a slice of all tiles matching the given style,
// sequence and tileType. // sequence and tileType.
func (m *MapEngine) GetTiles(style, sequence int, tileType d2enum.TileType) []d2dt1.Tile { func (m *MapEngine) GetTiles(style, sequence int, tileType d2enum.TileType) []d2dt1.Tile {
tiles := make([]d2dt1.Tile, 0, len(m.dt1TileData)) tiles := make([]d2dt1.Tile, 0)
for idx := range m.dt1TileData { for idx := range m.dt1TileData {
if m.dt1TileData[idx].Style != int32(style) || m.dt1TileData[idx].Sequence != int32(sequence) || if m.dt1TileData[idx].Style != int32(style) || m.dt1TileData[idx].Sequence != int32(sequence) ||

View File

@ -64,7 +64,7 @@ func NewAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEntit
// NewPlayer creates a new player entity and returns a pointer to it. // NewPlayer creates a new player entity and returns a pointer to it.
func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero, func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero,
stats *d2hero.HeroStatsState, skills map[int]*d2hero.HeroSkill, equipment *d2inventory.CharacterEquipment, stats *d2hero.HeroStatsState, skills map[int]*d2hero.HeroSkill, equipment *d2inventory.CharacterEquipment,
leftSkill, rightSkill int, gold int) *Player { leftSkill, rightSkill, gold int) *Player {
layerEquipment := &[d2enum.CompositeTypeMax]string{ layerEquipment := &[d2enum.CompositeTypeMax]string{
d2enum.CompositeTypeHead: equipment.Head.GetArmorClass(), d2enum.CompositeTypeHead: equipment.Head.GetArmorClass(),
d2enum.CompositeTypeTorso: equipment.Torso.GetArmorClass(), d2enum.CompositeTypeTorso: equipment.Torso.GetArmorClass(),
@ -180,7 +180,7 @@ func (f *MapEntityFactory) NewItem(x, y int, codes ...string) (*Item, error) {
} }
// NewNPC creates a new NPC and returns a pointer to it. // NewNPC creates a new NPC and returns a pointer to it.
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatsRecord, direction int) (*NPC, error) { func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatRecord, direction int) (*NPC, error) {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/803 // https://github.com/OpenDiablo2/OpenDiablo2/issues/803
result := &NPC{ result := &NPC{
mapEntity: newMapEntity(x, y), mapEntity: newMapEntity(x, y),
@ -237,7 +237,6 @@ func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.Ove
return nil, err return nil, err
} }
// https://github.com/OpenDiablo2/OpenDiablo2/issues/767
animation.Rewind() animation.Rewind()
animation.ResetPlayedCount() animation.ResetPlayedCount()
@ -263,7 +262,7 @@ func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.Ove
} }
// NewObject creates an instance of AnimatedComposite // NewObject creates an instance of AnimatedComposite
func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailsRecord, func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailRecord,
palettePath string) (*Object, error) { palettePath string) (*Object, error) {
locX, locY := float64(x), float64(y) locX, locY := float64(x), float64(y)
entity := &Object{ entity := &Object{

View File

@ -22,8 +22,8 @@ type NPC struct {
action int action int
path int path int
repetitions int repetitions int
monstatRecord *d2records.MonStatsRecord monstatRecord *d2records.MonStatRecord
monstatEx *d2records.MonStats2Record monstatEx *d2records.MonStat2Record
HasPaths bool HasPaths bool
isDone bool isDone bool
} }

View File

@ -20,7 +20,7 @@ type Object struct {
composite *d2asset.Composite composite *d2asset.Composite
highlight bool highlight bool
// nameLabel d2ui.Label // nameLabel d2ui.Label
objectRecord *d2records.ObjectDetailsRecord objectRecord *d2records.ObjectDetailRecord
drawLayer int drawLayer int
name string name string
} }

View File

@ -2,8 +2,10 @@ package d2maprenderer
import ( import (
"errors" "errors"
"fmt"
"image/color" "image/color"
"math" "math"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
@ -86,20 +88,11 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
result.Camera.position = &startPosition result.Camera.position = &startPosition
result.viewport.SetCamera(&result.Camera) result.viewport.SetCamera(&result.Camera)
var err error if err := term.Bind("mapdebugvis", "set map debug visualization level", nil, result.commandMapDebugVis); err != nil {
err = term.BindAction("mapdebugvis", "set map debug visualization level", func(level int) {
result.mapDebugVisLevel = level
})
if err != nil {
result.Errorf("could not bind the mapdebugvis action, err: %v", err) result.Errorf("could not bind the mapdebugvis action, err: %v", err)
} }
err = term.BindAction("entitydebugvis", "set entity debug visualization level", func(level int) { if err := term.Bind("entitydebugvis", "set entity debug visualization level", nil, result.commandEntityDebugVis); err != nil {
result.entityDebugVisLevel = level
})
if err != nil {
result.Errorf("could not bind the entitydebugvis action, err: %v", err) 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 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(). // RegenerateTileCache calls MapRenderer.generateTileCache().
func (mr *MapRenderer) RegenerateTileCache() { func (mr *MapRenderer) RegenerateTileCache() {
mr.generateTileCache() mr.generateTileCache()

View File

@ -22,7 +22,7 @@ func armorTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Animation.Token.Armor = records r.Animation.Token.Armor = records
r.Logger.Infof("Loaded %d ArmorType records", len(records)) r.Debugf("Loaded %d ArmorType records", len(records))
return nil return nil
} }

View File

@ -79,7 +79,7 @@ func autoMagicLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d AutoMagic records", len(records)) r.Debugf("Loaded %d AutoMagic records", len(records))
r.Item.AutoMagic = records r.Item.AutoMagic = records

View File

@ -37,7 +37,7 @@ func autoMapLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d AutoMapRecord records", len(records)) r.Debugf("Loaded %d AutoMap records", len(records))
r.Level.AutoMaps = records r.Level.AutoMaps = records

View File

@ -102,7 +102,7 @@ func beltsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d belts", len(records)) r.Debugf("Loaded %d Belt records", len(records))
r.Item.Belts = records r.Item.Belts = records

View File

@ -19,7 +19,7 @@ func bodyLocationsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
panic(d.Err) panic(d.Err)
} }
r.Logger.Infof("Loaded %d Body Location records", len(records)) r.Debugf("Loaded %d BodyLocation records", len(records))
r.BodyLocations = records r.BodyLocations = records

View File

@ -8,7 +8,7 @@ func booksLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(Books) records := make(Books)
for d.Next() { for d.Next() {
record := &BooksRecord{ record := &BookRecord{
Name: d.String("Name"), Name: d.String("Name"),
Namco: d.String("Namco"), Namco: d.String("Namco"),
Completed: d.String("Completed"), Completed: d.String("Completed"),
@ -28,7 +28,7 @@ func booksLoader(r *RecordManager, d *d2txt.DataDictionary) error {
panic(d.Err) panic(d.Err)
} }
r.Logger.Infof("Loaded %d book items", len(records)) r.Debugf("Loaded %d Book records", len(records))
r.Item.Books = records r.Item.Books = records

View File

@ -1,10 +1,10 @@
package d2records package d2records
// Books stores all of the BooksRecords // Books stores all of the BookRecords
type Books map[string]*BooksRecord type Books map[string]*BookRecord
// BooksRecord is a representation of a row from books.txt // BookRecord is a representation of a row from books.txt
type BooksRecord struct { type BookRecord struct {
Name string Name string
Namco string // The displayed name, where the string prefix is "Tome" Namco string // The displayed name, where the string prefix is "Tome"
Completed string Completed string

View File

@ -5,32 +5,28 @@ import (
) )
func skillCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error { func skillCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records, err := loadCalculations(r, d) records, err := loadCalculations(r, d, "Skill")
if err != nil { if err != nil {
return err return err
} }
r.Logger.Infof("Loaded %d Skill Calculation records", len(records))
r.Calculation.Skills = records r.Calculation.Skills = records
return nil return nil
} }
func missileCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error { func missileCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records, err := loadCalculations(r, d) records, err := loadCalculations(r, d, "Missile")
if err != nil { if err != nil {
return err return err
} }
r.Logger.Infof("Loaded %d Missile Calculation records", len(records))
r.Calculation.Missiles = records r.Calculation.Missiles = records
return nil return nil
} }
func loadCalculations(r *RecordManager, d *d2txt.DataDictionary) (Calculations, error) { func loadCalculations(r *RecordManager, d *d2txt.DataDictionary, name string) (Calculations, error) {
records := make(Calculations) records := make(Calculations)
for d.Next() { for d.Next() {
@ -45,7 +41,7 @@ func loadCalculations(r *RecordManager, d *d2txt.DataDictionary) (Calculations,
return nil, d.Err return nil, d.Err
} }
r.Logger.Infof("Loaded %d Skill Calculation records", len(records)) r.Debugf("Loaded %d %s Calculation records", len(records), name)
return records, nil return records, nil
} }

View File

@ -38,7 +38,7 @@ func charStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
} }
for d.Next() { for d.Next() {
record := &CharStatsRecord{ record := &CharStatRecord{
Class: stringMap[d.String("class")], Class: stringMap[d.String("class")],
InitStr: d.Number("str"), InitStr: d.Number("str"),
@ -136,7 +136,7 @@ func charStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d CharStats records", len(records)) r.Debugf("Loaded %d CharStat records", len(records))
r.Character.Stats = records r.Character.Stats = records

View File

@ -2,11 +2,11 @@ package d2records
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// CharStats holds all of the CharStatsRecords // CharStats holds all of the CharStatRecords
type CharStats map[d2enum.Hero]*CharStatsRecord type CharStats map[d2enum.Hero]*CharStatRecord
// CharStatsRecord is a struct that represents a single row from charstats.txt // CharStatRecord is a struct that represents a single row from charstats.txt
type CharStatsRecord struct { type CharStatRecord struct {
Class d2enum.Hero Class d2enum.Hero
// the initial stats at character level 1 // the initial stats at character level 1

View File

@ -22,7 +22,7 @@ func colorsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Colors = records r.Colors = records
r.Logger.Infof("Loaded %d Color records", len(records)) r.Debugf("Loaded %d Color records", len(records))
return nil return nil
} }

View File

@ -19,7 +19,7 @@ func componentCodesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d ComponentCode records", len(records)) r.Debugf("Loaded %d ComponentCode records", len(records))
r.ComponentCodes = records r.ComponentCodes = records

View File

@ -22,7 +22,7 @@ func compositeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Animation.Token.Composite = records r.Animation.Token.Composite = records
r.Logger.Infof("Loaded %d Composite Type records", len(records)) r.Debugf("Loaded %d CompositeType records", len(records))
return nil return nil
} }

View File

@ -22,7 +22,7 @@ func cubeModifierLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.Cube.Modifiers = records r.Item.Cube.Modifiers = records
r.Logger.Infof("Loaded %d Cube Modifier records", len(records)) r.Debugf("Loaded %d CubeModifier records", len(records))
return nil return nil
} }

View File

@ -22,7 +22,7 @@ func cubeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.Cube.Types = records r.Item.Cube.Types = records
r.Logger.Infof("Loaded %d Cube Type records", len(records)) r.Debugf("Loaded %d CubeType records", len(records))
return nil return nil
} }

View File

@ -96,7 +96,7 @@ func cubeRecipeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d CubeMainRecord records", len(records)) r.Debugf("Loaded %d CubeRecipe records", len(records))
r.Item.Cube.Recipes = records r.Item.Cube.Recipes = records

View File

@ -42,7 +42,7 @@ func difficultyLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d DifficultyLevel records", len(records)) r.Debugf("Loaded %d DifficultyLevel records", len(records))
r.DifficultyLevels = records r.DifficultyLevels = records

View File

@ -20,7 +20,7 @@ func elemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d ElemType records", len(records)) r.Debugf("Loaded %d ElemType records", len(records))
r.ElemTypes = records r.ElemTypes = records

View File

@ -20,7 +20,7 @@ func eventsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d Event records", len(records)) r.Debugf("Loaded %d Event records", len(records))
r.Character.Events = records r.Character.Events = records

View File

@ -48,7 +48,7 @@ func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
} }
for d.Next() { for d.Next() {
record := &ExperienceBreakpointsRecord{ record := &ExperienceBreakpointRecord{
Level: d.Number("Level"), Level: d.Number("Level"),
HeroBreakpoints: map[d2enum.Hero]int{ HeroBreakpoints: map[d2enum.Hero]int{
d2enum.HeroAmazon: d.Number("Amazon"), d2enum.HeroAmazon: d.Number("Amazon"),
@ -68,7 +68,7 @@ func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d Experience Breakpoint records", len(breakpoints)) r.Debugf("Loaded %d ExperienceBreakpoint records", len(breakpoints))
r.Character.MaxLevel = maxLevels r.Character.MaxLevel = maxLevels
r.Character.Experience = breakpoints r.Character.Experience = breakpoints

View File

@ -4,14 +4,14 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// ExperienceBreakpoints describes the required experience // ExperienceBreakpoints describes the required experience
// for each level for each character class // for each level for each character class
type ExperienceBreakpoints map[int]*ExperienceBreakpointsRecord type ExperienceBreakpoints map[int]*ExperienceBreakpointRecord
// ExperienceMaxLevels defines the max character levels // ExperienceMaxLevels defines the max character levels
type ExperienceMaxLevels map[d2enum.Hero]int type ExperienceMaxLevels map[d2enum.Hero]int
// ExperienceBreakpointsRecord describes the experience points required to // ExperienceBreakpointRecord describes the experience points required to
// gain a level for all character classes // gain a level for all character classes
type ExperienceBreakpointsRecord struct { type ExperienceBreakpointRecord struct {
Level int Level int
HeroBreakpoints map[d2enum.Hero]int HeroBreakpoints map[d2enum.Hero]int
Ratio int Ratio int

View File

@ -19,7 +19,7 @@ func gambleLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d gamble records", len(records)) r.Debugf("Loaded %d Gamble records", len(records))
r.Gamble = records r.Gamble = records

View File

@ -4,12 +4,12 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
) )
// LoadGems loads gem records into a map[string]*GemsRecord // LoadGems loads gem records into a map[string]*GemRecord
func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error { func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(Gems) records := make(Gems)
for d.Next() { for d.Next() {
gem := &GemsRecord{ gem := &GemRecord{
Name: d.String("name"), Name: d.String("name"),
Letter: d.String("letter"), Letter: d.String("letter"),
Transform: d.Number("transform"), Transform: d.Number("transform"),
@ -60,7 +60,7 @@ func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d Gems records", len(records)) r.Debugf("Loaded %d Gem records", len(records))
r.Item.Gems = records r.Item.Gems = records

View File

@ -1,11 +1,11 @@
package d2records package d2records
// Gems stores all of the GemsRecords // Gems stores all of the GemRecords
type Gems map[string]*GemsRecord type Gems map[string]*GemRecord
// GemsRecord is a representation of a single row of gems.txt // GemRecord is a representation of a single row of gems.txt
// it describes the properties of socketable items // it describes the properties of socketable items
type GemsRecord struct { type GemRecord struct {
Name string Name string
Letter string Letter string
Transform int Transform int

View File

@ -22,7 +22,7 @@ func hirelingDescriptionLoader(r *RecordManager, d *d2txt.DataDictionary) error
r.Hireling.Descriptions = records r.Hireling.Descriptions = records
r.Logger.Infof("Loaded %d Hireling Descriptions records", len(records)) r.Debugf("Loaded %d HirelingDescription records", len(records))
return nil return nil
} }

View File

@ -90,7 +90,7 @@ func hirelingLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d Hireling records", len(records)) r.Debugf("Loaded %d Hireling records", len(records))
r.Hireling.Details = records r.Hireling.Details = records

View File

@ -22,7 +22,7 @@ func hitClassLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Animation.Token.HitClass = records r.Animation.Token.HitClass = records
r.Logger.Infof("Loaded %d HitClass records", len(records)) r.Debugf("Loaded %d HitClass records", len(records))
return nil return nil
} }

View File

@ -130,7 +130,7 @@ func inventoryLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d Inventory Panel records", len(records)) r.Debugf("Loaded %d Inventory records", len(records))
r.Layout.Inventory = records r.Layout.Inventory = records

View File

@ -70,7 +70,7 @@ func loadAffixDictionary(
} }
name := getAffixString(superType, subType) name := getAffixString(superType, subType)
r.Logger.Infof("Loaded %d %s records", len(records), name) r.Debugf("Loaded %d %s records", len(records), name)
return records, groups, nil return records, groups, nil
} }

View File

@ -16,7 +16,7 @@ func armorLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return err return err
} }
r.Logger.Infof("Loaded %d armors", len(records)) r.Debugf("Loaded %d Armor Item records", len(records))
r.Item.Armors = records r.Item.Armors = records

View File

@ -21,7 +21,7 @@ func lowQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.LowQualityPrefixes = records r.Item.LowQualityPrefixes = records
r.Logger.Infof("Loaded %d Low Item Quality records", len(records)) r.Debugf("Loaded %d LowQuality records", len(records))
return nil return nil
} }

View File

@ -13,7 +13,7 @@ func miscItemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return err return err
} }
r.Logger.Infof("Loaded %d misc items", len(records)) r.Debugf("Loaded %d Misc Item records", len(records))
r.Item.Misc = records r.Item.Misc = records

View File

@ -45,7 +45,7 @@ func itemQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
r.Item.Quality = records r.Item.Quality = records
r.Logger.Infof("Loaded %d ItemQualities records", len(records)) r.Debugf("Loaded %d ItemQuality records", len(records))
return nil return nil
} }

View File

@ -55,7 +55,7 @@ func itemRatioLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d ItemRatio records", len(records)) r.Debugf("Loaded %d ItemRatio records", len(records))
r.Item.Ratios = records r.Item.Ratios = records

View File

@ -76,7 +76,7 @@ func itemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d ItemType records", len(records)) r.Debugf("Loaded %d ItemType records", len(records))
r.Item.Types = records r.Item.Types = records
r.Item.Equivalency = equivMap r.Item.Equivalency = equivMap

View File

@ -13,7 +13,7 @@ func weaponsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return err return err
} }
r.Logger.Infof("Loaded %d weapons", len(records)) r.Debugf("Loaded %d Weapon records", len(records))
r.Item.Weapons = records r.Item.Weapons = records

View File

@ -95,7 +95,7 @@ func itemStatCostLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d ItemStatCost records", len(records)) r.Debugf("Loaded %d ItemStatCost records", len(records))
r.Item.Stats = records r.Item.Stats = records

View File

@ -11,7 +11,7 @@ func levelDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(LevelDetails) records := make(LevelDetails)
for d.Next() { for d.Next() {
record := &LevelDetailsRecord{ record := &LevelDetailRecord{
Name: d.String("Name "), Name: d.String("Name "),
ID: d.Number("Id"), ID: d.Number("Id"),
Palette: d.Number("Pal"), Palette: d.Number("Pal"),
@ -165,7 +165,7 @@ func levelDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d LevelDetails records", len(records)) r.Debugf("Loaded %d LevelDetail records", len(records))
r.Level.Details = records r.Level.Details = records

View File

@ -2,13 +2,13 @@ package d2records
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// LevelDetails has all of the LevelDetailsRecords // LevelDetails has all of the LevelDetailRecords
type LevelDetails map[int]*LevelDetailsRecord type LevelDetails map[int]*LevelDetailRecord
// LevelDetailsRecord is a representation of a row from levels.txt // LevelDetailRecord is a representation of a row from levels.txt
// it describes lots of things about the levels, like where they are connected, // it describes lots of things about the levels, like where they are connected,
// what kinds of monsters spawn, the level generator type, and lots of other stuff. // what kinds of monsters spawn, the level generator type, and lots of other stuff.
type LevelDetailsRecord struct { type LevelDetailRecord struct {
// Name // Name
// This column has no function, it only serves as a comment field to make it // This column has no function, it only serves as a comment field to make it

View File

@ -8,7 +8,7 @@ func levelMazeDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(LevelMazeDetails) records := make(LevelMazeDetails)
for d.Next() { for d.Next() {
record := &LevelMazeDetailsRecord{ record := &LevelMazeDetailRecord{
Name: d.String("Name"), Name: d.String("Name"),
LevelID: d.Number("Level"), LevelID: d.Number("Level"),
NumRoomsNormal: d.Number("Rooms"), NumRoomsNormal: d.Number("Rooms"),
@ -24,7 +24,7 @@ func levelMazeDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d LevelMazeDetails records", len(records)) r.Debugf("Loaded %d LevelMazeDetail records", len(records))
r.Level.Maze = records r.Level.Maze = records

View File

@ -1,11 +1,11 @@
package d2records package d2records
// LevelMazeDetails stores all of the LevelMazeDetailsRecords // LevelMazeDetails stores all of the LevelMazeDetailRecords
type LevelMazeDetails map[int]*LevelMazeDetailsRecord type LevelMazeDetails map[int]*LevelMazeDetailRecord
// LevelMazeDetailsRecord is a representation of a row from lvlmaze.txt // LevelMazeDetailRecord is a representation of a row from lvlmaze.txt
// these records define the parameters passed to the maze level generator // these records define the parameters passed to the maze level generator
type LevelMazeDetailsRecord struct { type LevelMazeDetailRecord struct {
// descriptive, not loaded in game. Corresponds with Name field in // descriptive, not loaded in game. Corresponds with Name field in
// Levels.txt // Levels.txt
Name string // Name Name string // Name

View File

@ -42,7 +42,7 @@ func levelPresetLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records[record.DefinitionID] = record records[record.DefinitionID] = record
} }
r.Logger.Infof("Loaded %d level presets", len(records)) r.Debugf("Loaded %d LevelPresets records", len(records))
if d.Err != nil { if d.Err != nil {
return d.Err return d.Err

View File

@ -40,7 +40,7 @@ func levelSubstitutionsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d LevelSubstitution records", len(records)) r.Debugf("Loaded %d LevelSubstitution records", len(records))
r.Level.Sub = records r.Level.Sub = records

View File

@ -58,7 +58,7 @@ func levelTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d LevelType records", len(records)) r.Debugf("Loaded %d LevelType records", len(records))
r.Level.Types = records r.Level.Types = records

View File

@ -30,7 +30,7 @@ func levelWarpsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d level warps", len(records)) r.Debugf("Loaded %d LevelWarp records", len(records))
r.Level.Warp = records r.Level.Warp = records

View File

@ -304,7 +304,7 @@ func missilesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d Missile Records", len(records)) r.Debugf("Loaded %d Missile records", len(records))
r.Missiles = records r.Missiles = records

View File

@ -19,7 +19,7 @@ func monsterAiLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d MonsterAI records", len(records)) r.Debugf("Loaded %d MonsterAI records", len(records))
r.Monster.AI = records r.Monster.AI = records

View File

@ -49,7 +49,7 @@ func monsterEquipmentLoader(r *RecordManager, d *d2txt.DataDictionary) error {
length += len(records[k]) length += len(records[k])
} }
r.Logger.Infof("Loaded %d MonsterEquipment records", length) r.Debugf("Loaded %d MonsterEquipment records", length)
r.Monster.Equipment = records r.Monster.Equipment = records

View File

@ -52,7 +52,7 @@ func monsterLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d MonsterLevel records", len(records)) r.Debugf("Loaded %d MonsterLevel records", len(records))
r.Monster.Levels = records r.Monster.Levels = records

View File

@ -21,7 +21,7 @@ func monsterModeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err return d.Err
} }
r.Logger.Infof("Loaded %d MonMode records", len(records)) r.Debugf("Loaded %d MonMode records", len(records))
r.Monster.Modes = records r.Monster.Modes = records

View File

@ -1,6 +1,6 @@
package d2records package d2records
// MonModes stores all of the GemsRecords // MonModes stores all of the MonModeRecords
type MonModes map[string]*MonModeRecord type MonModes map[string]*MonModeRecord
// MonModeRecord is a representation of a single row of Monmode.txt // MonModeRecord is a representation of a single row of Monmode.txt

Some files were not shown because too many files have changed in this diff Show More