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

Revert "Backmerge master into ecs (#1021)" (#1022)

This reverts commit 9121209f86.
This commit is contained in:
gravestench 2021-01-09 08:52:06 +00:00 committed by GitHub
parent 9121209f86
commit 99c7e2e754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 2393 additions and 2727 deletions

View File

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

39
.github/workflows/pushToMaster.yml vendored Normal file
View File

@ -0,0 +1,39 @@
---
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,8 +25,7 @@ ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
## Status ## Status
At the moment (december 2020) the game starts, you can select any character and run around Act1 town. At the moment (october 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.
@ -129,8 +128,6 @@ 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,7 +6,6 @@ import (
"container/ring" "container/ring"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"image" "image"
"image/gif" "image/gif"
@ -25,6 +24,7 @@ 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,12 +85,19 @@ 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 (
bytesToMegabyte = 1024 * 1024 bytesToMegabyte = 1024 * 1024
nSamplesTAlloc = 100 nSamplesTAlloc = 100
@ -103,24 +110,21 @@ 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 {
logger := d2util.NewLogger() assetManager, assetError := d2asset.NewAssetManager()
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.parseArguments() app.Logger = d2util.NewLogger()
app.Logger.SetPrefix(appLoggerPrefix)
app.SetLevel(*app.Options.LogLevel) app.Logger.SetLevel(d2util.LogLevelNone)
app.asset, app.errorMessage = d2asset.NewAssetManager(*app.Options.LogLevel)
return app return app
} }
@ -136,7 +140,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.Options.LogLevel, maxPlayers) srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, a.config.LogLevel, maxPlayers)
if srvErr != nil { if srvErr != nil {
return srvErr return srvErr
} }
@ -169,7 +173,15 @@ 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")
} }
audio := ebiten2.CreateAudio(*a.Options.LogLevel, a.asset) // if the log level was specified at the command line, use it
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()
@ -178,9 +190,14 @@ 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.Options.LogLevel, audio) uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, a.config.LogLevel, audio)
a.inputManager = inputManager a.inputManager = inputManager
a.terminal = term a.terminal = term
@ -189,48 +206,50 @@ 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 (
descProfile = "Profiles the program,\none of (cpu, mem, block, goroutine, trace, thread, mutex)" versionArg = "version"
descPlayers = "Sets the number of max players for the dedicated server" versionShort = 'v'
descLogging = "Enables verbose logging. Log levels will include those below it.\n" + versionDesc = "Prints the version of the app"
" 0 disables log messages\n" +
" 1 shows fatal\n" + profilerArg = "profile"
" 2 shows error\n" + profilerDesc = "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)"
" 3 shows warning\n" +
" 4 shows info\n" + serverArg = "dedicated"
" 5 shows debug\n" serverShort = 'd'
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 = flag.String("profile", "", descProfile) a.Options.profiler = kingpin.Flag(profilerArg, profilerDesc).String()
a.Options.Server.Dedicated = flag.Bool("dedicated", false, "Starts a dedicated server") a.Options.Server.Dedicated = kingpin.Flag(serverArg, serverDesc).Short(serverShort).Bool()
a.Options.Server.MaxPlayers = flag.Int("players", 0, descPlayers) a.Options.printVersion = kingpin.Flag(versionArg, versionDesc).Short(versionShort).Bool()
a.Options.LogLevel = flag.Int("l", d2util.LogLevelDefault, descLogging) a.Options.Server.MaxPlayers = kingpin.Flag(playersArg, playersDesc).Int()
showVersion := flag.Bool("v", false, "Show version") a.Options.LogLevel = kingpin.Flag(loggingArg, loggingDesc).
showHelp := flag.Bool("h", false, "Show help") Short(loggingShort).
Default(strconv.Itoa(d2util.LogLevelUnspecified)).
Int()
flag.Usage = func() { kingpin.Parse()
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
@ -268,15 +287,45 @@ 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() (err error) { func (a *App) Run() 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()))
if a.config, err = a.LoadConfig(); err != nil { config, err := a.LoadConfig()
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)
@ -340,39 +389,36 @@ func (a *App) initialize() error {
a.renderer.SetWindowIcon("d2logo.png") a.renderer.SetWindowIcon("d2logo.png")
a.terminal.BindLogger() a.terminal.BindLogger()
terminalCommands := []struct { terminalActions := [...]bindTerminalEntry{
name string {"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap},
desc string {"fullscreen", "toggles fullscreen", a.toggleFullScreen},
args []string {"capframe", "captures a still frame", a.setupCaptureFrame},
fn func(args []string) error {"capgifstart", "captures an animation (start)", a.startAnimationCapture},
}{ {"capgifstop", "captures an animation (stop)", a.stopAnimationCapture},
{"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap}, {"vsync", "toggles vsync", a.toggleVsync},
{"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen}, {"fps", "toggle fps counter", a.toggleFpsCounter},
{"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame}, {"timescale", "set scalar for elapsed time", a.setTimeScale},
{"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture}, {"quit", "exits the game", a.quitGame},
{"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture}, {"screen-gui", "enters the gui playground screen", a.enterGuiPlayground},
{"vsync", "toggles vsync", nil, a.toggleVsync}, {"js", "eval JS scripts", a.evalJS},
{"fps", "toggle fps counter", nil, a.toggleFpsCounter},
{"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale},
{"quit", "exits the game", nil, a.quitGame},
{"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground},
{"js", "eval JS scripts", []string{"code"}, a.evalJS},
} }
for _, cmd := range terminalCommands { for idx := range terminalActions {
if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil { action := &terminalActions[idx]
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.Options.LogLevel, a.inputManager) gui, err := d2gui.CreateGuiManager(a.asset, a.config.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.Options.LogLevel, a.guiManager) a.screen = d2screen.NewScreenManager(a.ui, a.config.LogLevel, a.guiManager)
a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume) a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)
@ -636,7 +682,7 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
return deltaAllocPerFrame * fps / bytesToMegabyte return deltaAllocPerFrame * fps / bytesToMegabyte
} }
func (a *App) dumpHeap([]string) error { func (a *App) dumpHeap() {
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())
@ -655,56 +701,48 @@ func (a *App) dumpHeap([]string) error {
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(args []string) error { func (a *App) evalJS(code string) {
val, err := a.scriptEngine.Eval(args[0]) val, err := a.scriptEngine.Eval(code)
if err != nil { if err != nil {
a.terminal.Errorf(err.Error()) a.terminal.OutputErrorf("%s", err)
return nil return
} }
a.Info("%s" + val) a.Info("%s" + val)
return nil
} }
func (a *App) toggleFullScreen([]string) error { func (a *App) toggleFullScreen() {
fullscreen := !a.renderer.IsFullScreen() fullscreen := !a.renderer.IsFullScreen()
a.renderer.SetFullScreen(fullscreen) a.renderer.SetFullScreen(fullscreen)
a.terminal.Infof("fullscreen is now: %v", fullscreen) a.terminal.OutputInfof("fullscreen is now: %v", fullscreen)
return nil
} }
func (a *App) setupCaptureFrame(args []string) error { func (a *App) setupCaptureFrame(path string) {
a.captureState = captureStateFrame a.captureState = captureStateFrame
a.capturePath = args[0] a.capturePath = path
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
} }
if err := fp.Close(); err != nil { a.Info(fmt.Sprintf("saved frame to %s", a.capturePath))
a.terminal.Errorf("failed to create %q", a.capturePath)
return nil
}
a.terminal.Infof("saved frame to %s", a.capturePath)
return nil return nil
} }
@ -764,61 +802,47 @@ func (a *App) convertFramesToGif() error {
return err return err
} }
a.Infof("saved animation to %s", a.capturePath) a.Info(fmt.Sprintf("saved animation to %s", a.capturePath))
return nil return nil
} }
func (a *App) startAnimationCapture(args []string) error { func (a *App) startAnimationCapture(path string) {
a.captureState = captureStateGif a.captureState = captureStateGif
a.capturePath = args[0] a.capturePath = path
a.captureFrames = nil a.captureFrames = nil
return nil
} }
func (a *App) stopAnimationCapture([]string) error { func (a *App) stopAnimationCapture() {
a.captureState = captureStateNone a.captureState = captureStateNone
return nil
} }
func (a *App) toggleVsync([]string) error { func (a *App) toggleVsync() {
vsync := !a.renderer.GetVSyncEnabled() vsync := !a.renderer.GetVSyncEnabled()
a.renderer.SetVSyncEnabled(vsync) a.renderer.SetVSyncEnabled(vsync)
a.terminal.Infof("vsync is now: %v", vsync) a.terminal.OutputInfof("vsync is now: %v", vsync)
return nil
} }
func (a *App) toggleFpsCounter([]string) error { func (a *App) toggleFpsCounter() {
a.showFPS = !a.showFPS a.showFPS = !a.showFPS
a.terminal.Infof("fps counter is now: %v", a.showFPS) a.terminal.OutputInfof("fps counter is now: %v", a.showFPS)
return nil
} }
func (a *App) setTimeScale(args []string) error { func (a *App) setTimeScale(timeScale float64) {
timeScale, err := strconv.ParseFloat(args[0], 64) if timeScale <= 0 {
if err != nil || timeScale <= 0 { a.terminal.OutputErrorf("invalid time scale value")
a.terminal.Errorf("invalid time scale value") } else {
return nil a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale)
}
a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale)
a.timeScale = timeScale a.timeScale = timeScale
}
return nil
} }
func (a *App) quitGame([]string) error { func (a *App) quitGame() {
os.Exit(0) os.Exit(0)
return nil
} }
func (a *App) enterGuiPlayground([]string) error { func (a *App) enterGuiPlayground() {
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset)) a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, a.config.LogLevel, a.asset))
return nil
} }
func createZeroedRing(n int) *ring.Ring { func createZeroedRing(n int) *ring.Ring {
@ -887,7 +911,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.Options.LogLevel, errorMessageOptional...) a.config.LogLevel, errorMessageOptional...)
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
return return
@ -898,7 +922,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.Options.LogLevel, host) selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, a.config.LogLevel, host)
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
return return
@ -909,18 +933,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.Options.LogLevel, a.scriptEngine) gameClient, err := d2client.Create(connType, a.asset, a.config.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)
a.Error(errorMessage) fmt.Println(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.Options.LogLevel, a.guiManager, a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, a.config.LogLevel, a.guiManager,
) )
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
@ -933,9 +957,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.Options.LogLevel, connHost) a.audio, a.ui, connType, a.config.LogLevel, connHost)
if err != nil { if err != nil {
a.Errorf("unable to create character select screen: %s", err) fmt.Printf("unable to create character select screen: %s", err)
} }
a.screen.SetNextScreen(characterSelect) a.screen.SetNextScreen(characterSelect)
@ -944,7 +968,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.Options.LogLevel, a.screen) a.config.LogLevel, a.screen)
if err != nil { if err != nil {
a.Error(err.Error()) a.Error(err.Error())
return return
@ -955,10 +979,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.Options.LogLevel, a.ui)) a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, a.config.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.Options.LogLevel, a.ui)) a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, a.config.LogLevel, a.ui))
} }

View File

@ -4,6 +4,12 @@ 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
@ -20,6 +26,16 @@ 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]
@ -28,46 +44,32 @@ 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 {
return int16(v.GetUInt16()) var result int16
for offset := uint64(0); offset < bytesPerInt16; offset++ {
shift := uint8(bitsPerByte * offset)
result += int16(v.data[v.position+offset]) << shift
} }
// GetUInt16 returns a uint16 word from the stream v.position += bytesPerInt16
//nolint
func (v *StreamReader) GetUInt16() uint16 {
b := v.ReadBytes(2)
return uint16(b[0]) | uint16(b[1])<<8
}
// GetInt32 returns an int32 dword from the stream return result
func (v *StreamReader) GetInt32() int32 {
return int32(v.GetUInt32())
}
// 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
@ -75,9 +77,51 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
v.position = newPosition v.position = newPosition
} }
// GetSize returns the total size of the stream in bytes // GetUInt32 returns a uint32 dword from the stream
func (v *StreamReader) GetSize() uint64 { func (v *StreamReader) GetUInt32() uint32 {
return uint64(len(v.data)) var result uint32
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,6 +2,10 @@ 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
@ -16,40 +20,41 @@ 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) {
v.PushUint16(uint16(val)) for count := 0; count < bytesPerInt16; count++ {
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) {
v.data.WriteByte(byte(val)) for count := 0; count < bytesPerInt32; count++ {
v.data.WriteByte(byte(val >> 8)) shift := count * bitsPerByte
v.data.WriteByte(byte(val >> 16)) v.data.WriteByte(byte(val>>shift) & byteMask)
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
@ -57,15 +62,7 @@ func (v *StreamWriter) PushInt64(val int64) {
v.PushUint64(uint64(val)) v.PushUint64(uint64(val))
} }
// PushUint64 writes a uint64 qword to the stream // GetBytes returns the the byte slice of the underlying data
//nolint func (v *StreamWriter) GetBytes() []byte {
func (v *StreamWriter) PushUint64(val uint64) { return v.data.Bytes()
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,9 +2,7 @@ package d2enum
// there are labels for "numeric labels (see AssetManager.TranslateLabel) // there are labels for "numeric labels (see AssetManager.TranslateLabel)
const ( const (
RepairAll = iota CancelLabel = iota
_
CancelLabel
CopyrightLabel CopyrightLabel
AllRightsReservedLabel AllRightsReservedLabel
SinglePlayerLabel SinglePlayerLabel
@ -64,8 +62,6 @@ 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

@ -1,11 +0,0 @@
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

@ -1,131 +0,0 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,35 @@
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,9 +2,10 @@ package d2mpq
import ( import (
"bufio" "bufio"
"encoding/binary"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -20,9 +21,31 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
type MPQ struct { type MPQ struct {
filePath string filePath string
file *os.File file *os.File
hashes map[uint64]*Hash hashEntryMap HashEntryMap
blocks []*Block blockTableEntries []BlockTableEntry
header Header data Data
}
// 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.
@ -30,153 +53,71 @@ 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
} }
// New loads an MPQ file and only reads the header // FileFlag represents flags for a file record in the MPQ archive
func New(fileName string) (*MPQ, error) { type FileFlag uint32
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" {
mpq.file, err = openIgnoreCase(fileName) result.file, err = openIgnoreCase(fileName)
} else { } else {
mpq.file, err = os.Open(fileName) //nolint:gosec // Will fix later result.file, err = os.Open(fileName) //nolint:gosec // Will fix later
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := mpq.readHeader(); err != nil { if err := result.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
} }
if err := mpq.readHashTable(); err != nil { return result, 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) {
@ -201,5 +142,258 @@ func openIgnoreCase(mpqPath string) (*os.File, error) {
} }
} }
return os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later file, err := 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

@ -1,77 +0,0 @@
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, err := m.stream.Read(p, 0, uint32(len(p))) totalRead := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), err return int(totalRead), nil
} }
// 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.Position = uint32(offset + int64(whence)) m.stream.CurrentPosition = uint32(offset + int64(whence))
return int64(m.stream.Position), nil return int64(m.stream.CurrentPosition), nil
} }
// Close closes the data stream // Close closes the data stream

View File

@ -1,45 +0,0 @@
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

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

View File

@ -8,9 +8,10 @@ type Archive interface {
Path() string Path() string
Contains(string) bool Contains(string) bool
Size() uint32 Size() uint32
Close() error Close()
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)
Listfile() ([]string, error) GetFileList() ([]string, error)
} }

View File

@ -1,5 +1,7 @@
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
@ -19,7 +21,7 @@ type Renderer interface {
GetCursorPos() (int, int) GetCursorPos() (int, int)
CurrentFPS() float64 CurrentFPS() float64
ShowPanicScreen(message string) ShowPanicScreen(message string)
Print(target interface{}, str string) error Print(target *ebiten.Image, str string) error
PrintAt(target interface{}, str string, x, y int) PrintAt(target *ebiten.Image, str string, x, y int)
GetWindowSize() (int, int) GetWindowSize() (int, int)
} }

View File

@ -2,14 +2,11 @@ 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
Rawf(category d2enum.TermCategory, format string, params ...interface{}) OutputRaw(text string, category d2enum.TermCategory)
Printf(format string, params ...interface{}) Outputf(format string, params ...interface{})
Infof(format string, params ...interface{}) OutputInfof(format string, params ...interface{})
Warningf(format string, params ...interface{}) OutputWarningf(format string, params ...interface{})
Errorf(format string, params ...interface{}) OutputErrorf(format string, params ...interface{})
Clear() OutputClear()
Visible() bool IsVisible() bool
Hide() Hide()
Show() Show()
Bind(name, description string, arguments []string, fn func(args []string) error) error BindAction(name, description string, action interface{}) error
Unbind(name ...string) error UnbindAction(name string) error
} }
// TerminalLogger is used tomake the Terminal write out // TerminalLogger is used tomake the Terminal write out

View File

@ -37,8 +37,7 @@ 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 mpq, err := d2mpq.New(path); err == nil { if _, err := d2mpq.Load(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.FromFile(sourcePath) loaded, err := d2mpq.Load(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. // was tested and works fine in main menu.
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) // checked "ITA": 0, // (Italian)
"JPN": 0, // (Japanese) "JPN": 0, // (Japanese)
"KOR": 0, // (Korean) "KOR": 0, // (Korean)
"SIN": 0, // "SIN": 0, //

View File

@ -193,7 +193,6 @@ 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 ---
@ -246,8 +245,6 @@ const (
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"
HeroStatsPanelStatsPoints = "/data/global/ui/PANEL/skillpoints.dc6"
HeroStatsPanelSocket = "/data/global/ui/PANEL/levelsocket.dc6"
InventoryWeaponsTab = "/data/global/ui/PANEL/invchar6Tab.DC6" InventoryWeaponsTab = "/data/global/ui/PANEL/invchar6Tab.DC6"
SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_back.DC6" SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_back.DC6"
SkillsPanelBarbarian = "/data/global/ui/SPELLS/skltree_b_back.DC6" SkillsPanelBarbarian = "/data/global/ui/SPELLS/skltree_b_back.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() || frameIndex < 0 { if frameIndex >= a.GetFrameCount() {
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 interface{}, str string) error { func (p *GlyphPrinter) Print(target *ebiten.Image, str string) error {
p.PrintAt(target.(*ebiten.Image), str, 0, 0) p.PrintAt(target, 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 interface{}, str string, x, y int) { func (p *GlyphPrinter) PrintAt(target *ebiten.Image, str string, x, y int) {
p.drawDebugText(target.(*ebiten.Image), str, x, y, false) p.drawDebugText(target, 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,7 +6,6 @@ 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)
@ -52,7 +51,6 @@ 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()
@ -66,7 +64,6 @@ 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.
@ -74,17 +71,11 @@ 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
} }
@ -94,9 +85,6 @@ 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
} }
@ -106,6 +94,10 @@ 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)
} }
@ -116,6 +108,10 @@ 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)
} }
@ -126,6 +122,10 @@ 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,6 +136,10 @@ 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)
} }
@ -147,6 +151,10 @@ 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)
} }
@ -156,10 +164,7 @@ func (l *Logger) Debugf(fmtMsg string, args ...interface{}) {
} }
func (l *Logger) print(level LogLevel, msg string) { func (l *Logger) print(level LogLevel, msg string) {
l.mutex.Lock() if l == nil || l.level < level {
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() || frameIndex < 0 { if frameIndex >= a.GetFrameCount() {
return errors.New("invalid frame index") return errors.New("invalid frame index")
} }

View File

@ -3,7 +3,6 @@ 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"
@ -410,70 +409,43 @@ 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.Bind("assetspam", "display verbose asset manager logs", nil, am.commandAssetSpam(term)); err != nil { if err := term.BindAction("assetspam", "display verbose asset manager logs", func(verbose bool) {
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.Infof("asset manager verbose logging enabled") term.OutputInfof("asset manager verbose logging enabled")
} else { } else {
term.Infof("asset manager verbose logging disabled") term.OutputInfof("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 nil return err
}
} }
func (am *AssetManager) commandAssetStat(term d2interface.Terminal) func([]string) error { if err := term.BindAction("assetstat", "display asset manager cache statistics", func() {
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.Infof("palette cache: %f", cacheStatistics(am.palettes)) term.OutputInfof("palette cache: %f", cacheStatistics(am.palettes))
term.Infof("palette transform cache: %f", cacheStatistics(am.transforms)) term.OutputInfof("palette transform cache: %f", cacheStatistics(am.transforms))
term.Infof("Animation cache: %f", cacheStatistics(am.animations)) term.OutputInfof("Animation cache: %f", cacheStatistics(am.animations))
term.Infof("font cache: %f", cacheStatistics(am.fonts)) term.OutputInfof("font cache: %f", cacheStatistics(am.fonts))
}); err != nil {
return nil return err
}
} }
func (am *AssetManager) commandAssetClear([]string) error { if err := term.BindAction("assetclear", "clear asset manager cache", func() {
am.palettes.Clear() am.palettes.Clear()
am.transforms.Clear() am.transforms.Clear()
am.animations.Clear() am.animations.Clear()
am.fonts.Clear() am.fonts.Clear()
}); err != nil {
return err
}
return nil return nil
} }

View File

@ -9,23 +9,19 @@ 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(logLevel d2util.LogLevel) (*AssetManager, error) { func NewAssetManager() (*AssetManager, error) {
loader, err := d2loader.NewLoader(logLevel) loader, err := d2loader.NewLoader(d2util.LogLevelDefault)
if err != nil { if err != nil {
return nil, err return nil, err
} }
records, err := d2records.NewRecordManager(logLevel) records, err := d2records.NewRecordManager(d2util.LogLevelDebug)
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: logger, Logger: d2util.NewLogger(),
Loader: loader, Loader: loader,
tables: make([]d2tbl.TextDictionary, 0), tables: make([]d2tbl.TextDictionary, 0),
animations: d2cache.CreateCache(animationBudget), animations: d2cache.CreateCache(animationBudget),
@ -35,5 +31,7 @@ func NewAssetManager(logLevel d2util.LogLevel) (*AssetManager, error) {
Records: records, Records: records,
} }
manager.SetPrefix(logPrefix)
return manager, err return manager, err
} }

View File

@ -2,6 +2,9 @@ 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"
@ -129,11 +132,24 @@ 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: dccDirection.Box.Width, width: frameWidth,
height: dccDirection.Box.Height, height: frameHeight,
offsetX: dccDirection.Box.Left, offsetX: minX,
offsetY: dccDirection.Box.Top, offsetY: minY,
decoded: true, decoded: true,
} }

View File

@ -3,7 +3,6 @@ package d2audio
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
@ -32,7 +31,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.SoundDetailRecord entry *d2records.SoundDetailsRecord
volume float64 volume float64
vTarget float64 vTarget float64
vRate float64 vRate float64
@ -104,11 +103,6 @@ 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
@ -134,25 +128,43 @@ func NewSoundEngine(provider d2interface.AudioProvider,
r.Logger.SetPrefix(logPrefix) r.Logger.SetPrefix(logPrefix)
r.Logger.SetLevel(l) r.Logger.SetLevel(l)
if err := term.Bind("playsoundid", "plays the sound for a given id", []string{"id"}, r.commandPlaySoundID); err != nil { err := term.BindAction("playsoundid", "plays the sound for a given id", func(id int) {
r.PlaySoundID(id)
})
if err != nil {
r.Error(err.Error()) r.Error(err.Error())
return nil return nil
} }
if err := term.Bind("playsound", "plays the sound for a given handle string", []string{"name"}, r.commandPlaySound); err != nil { err = term.BindAction("playsound", "plays the sound for a given handle string", func(handle string) {
r.PlaySoundHandle(handle)
})
if err != nil {
r.Error(err.Error()) r.Error(err.Error())
return nil return nil
} }
if err := term.Bind("activesounds", "list currently active sounds", nil, r.commandActiveSounds); err != nil { err = term.BindAction("activesounds", "list currently active sounds", func() {
for s := range r.sounds {
if err != nil {
r.Error(err.Error()) r.Error(err.Error())
return nil return
} }
if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil { r.Info(fmt.Sprint(s))
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
} }
@ -182,11 +194,6 @@ 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 {
@ -235,35 +242,3 @@ 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,8 +13,7 @@ type CommandRegistration struct {
Enabled bool Enabled bool
Name string Name string
Description string Description string
Args []string Callback interface{}
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,6 +5,8 @@ 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
@ -19,6 +21,7 @@ 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,6 +4,8 @@ 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
@ -35,6 +37,7 @@ func DefaultConfig() *Configuration {
"d2video.mpq", "d2video.mpq",
"d2speech.mpq", "d2speech.mpq",
}, },
LogLevel: d2util.LogLevelDefault,
path: DefaultConfigPath(), path: DefaultConfigPath(),
} }

View File

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

View File

@ -1,6 +1,8 @@
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"
) )
@ -35,3 +37,28 @@ 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 := d2util.Color(white) drawColor := rgbaColor(white)
switch entry.widget.(type) { switch entry.widget.(type) {
case *Layout: case *Layout:
drawColor = d2util.Color(magenta) drawColor = rgbaColor(magenta)
case *SpacerStatic, *SpacerDynamic: case *SpacerStatic, *SpacerDynamic:
drawColor = d2util.Color(grey2) drawColor = rgbaColor(grey2)
case *Label: case *Label:
drawColor = d2util.Color(green) drawColor = rgbaColor(green)
case *Button: case *Button:
drawColor = d2util.Color(yellow) drawColor = rgbaColor(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 := d2util.Color(grey) textColor := rgbaColor(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,7 +64,10 @@ 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(parentLayout, targetLayout *Layout) *LayoutScrollbar { func NewLayoutScrollbar(
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.CharStatRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) { func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatsRecord, 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,27 +10,32 @@ type HeroStatsState struct {
Level int `json:"level"` Level int `json:"level"`
Experience int `json:"experience"` Experience int `json:"experience"`
Strength int `json:"strength"`
Energy int `json:"energy"`
Dexterity int `json:"dexterity"`
Vitality int `json:"vitality"` Vitality int `json:"vitality"`
// there are stats and skills points remaining to add. Energy int `json:"energy"`
StatsPoints int `json:"statsPoints"` Strength int `json:"strength"`
SkillPoints int `json:"skillPoints"` Dexterity int `json:"dexterity"`
AttackRating int `json:"attackRating"`
DefenseRating int `json:"defenseRating"`
MaxStamina int `json:"maxStamina"`
Health int `json:"health"` Health int `json:"health"`
MaxHealth int `json:"maxHealth"` MaxHealth int `json:"maxHealth"`
Mana int `json:"mana"` Mana int `json:"mana"`
MaxMana int `json:"maxMana"` MaxMana int `json:"maxMana"`
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
MaxStamina int `json:"maxStamina"` 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.CharStatRecord) *HeroStatsState { func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2records.CharStatsRecord) *HeroStatsState {
result := HeroStatsState{ result := HeroStatsState{
Level: 1, Level: 1,
Experience: 0, Experience: 0,
@ -39,8 +44,6 @@ 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,6 +136,7 @@ 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.CharStatRecord{ var charStats = map[d2enum.Hero]*d2records.CharStatsRecord{
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.MonStatRecord{ var monStats = map[string]*d2records.MonStatsRecord{
"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) tiles := make([]d2dt1.Tile, 0, len(m.dt1TileData))
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, gold int) *Player { leftSkill, rightSkill int, 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.MonStatRecord, direction int) (*NPC, error) { func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatsRecord, 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,6 +237,7 @@ 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()
@ -262,7 +263,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.ObjectDetailRecord, func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailsRecord,
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.MonStatRecord monstatRecord *d2records.MonStatsRecord
monstatEx *d2records.MonStat2Record monstatEx *d2records.MonStats2Record
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.ObjectDetailRecord objectRecord *d2records.ObjectDetailsRecord
drawLayer int drawLayer int
name string name string
} }

View File

@ -2,10 +2,8 @@ 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"
@ -88,11 +86,20 @@ 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)
if err := term.Bind("mapdebugvis", "set map debug visualization level", nil, result.commandMapDebugVis); err != nil { var err error
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)
} }
if err := term.Bind("entitydebugvis", "set entity debug visualization level", nil, result.commandEntityDebugVis); err != nil { err = term.BindAction("entitydebugvis", "set entity debug visualization level", func(level int) {
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)
} }
@ -103,33 +110,6 @@ 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.Debugf("Loaded %d ArmorType records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d AutoMagic records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d AutoMap records", len(records)) r.Logger.Infof("Loaded %d AutoMapRecord 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.Debugf("Loaded %d Belt records", len(records)) r.Logger.Infof("Loaded %d belts", 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.Debugf("Loaded %d BodyLocation records", len(records)) r.Logger.Infof("Loaded %d Body Location 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 := &BookRecord{ record := &BooksRecord{
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.Debugf("Loaded %d Book records", len(records)) r.Logger.Infof("Loaded %d book items", 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 BookRecords // Books stores all of the BooksRecords
type Books map[string]*BookRecord type Books map[string]*BooksRecord
// BookRecord is a representation of a row from books.txt // BooksRecord is a representation of a row from books.txt
type BookRecord struct { type BooksRecord 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,28 +5,32 @@ import (
) )
func skillCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error { func skillCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records, err := loadCalculations(r, d, "Skill") records, err := loadCalculations(r, d)
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, "Missile") records, err := loadCalculations(r, d)
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, name string) (Calculations, error) { func loadCalculations(r *RecordManager, d *d2txt.DataDictionary) (Calculations, error) {
records := make(Calculations) records := make(Calculations)
for d.Next() { for d.Next() {
@ -41,7 +45,7 @@ func loadCalculations(r *RecordManager, d *d2txt.DataDictionary, name string) (C
return nil, d.Err return nil, d.Err
} }
r.Debugf("Loaded %d %s Calculation records", len(records), name) r.Logger.Infof("Loaded %d Skill Calculation records", len(records))
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 := &CharStatRecord{ record := &CharStatsRecord{
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.Debugf("Loaded %d CharStat records", len(records)) r.Logger.Infof("Loaded %d CharStats 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 CharStatRecords // CharStats holds all of the CharStatsRecords
type CharStats map[d2enum.Hero]*CharStatRecord type CharStats map[d2enum.Hero]*CharStatsRecord
// CharStatRecord is a struct that represents a single row from charstats.txt // CharStatsRecord is a struct that represents a single row from charstats.txt
type CharStatRecord struct { type CharStatsRecord 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.Debugf("Loaded %d Color records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d ComponentCode records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d CompositeType records", len(records)) r.Logger.Infof("Loaded %d Composite Type 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.Debugf("Loaded %d CubeModifier records", len(records)) r.Logger.Infof("Loaded %d Cube Modifier 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.Debugf("Loaded %d CubeType records", len(records)) r.Logger.Infof("Loaded %d Cube Type 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.Debugf("Loaded %d CubeRecipe records", len(records)) r.Logger.Infof("Loaded %d CubeMainRecord 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.Debugf("Loaded %d DifficultyLevel records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d ElemType records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d Event records", len(records)) r.Logger.Infof("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 := &ExperienceBreakpointRecord{ record := &ExperienceBreakpointsRecord{
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.Debugf("Loaded %d ExperienceBreakpoint records", len(breakpoints)) r.Logger.Infof("Loaded %d Experience Breakpoint 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]*ExperienceBreakpointRecord type ExperienceBreakpoints map[int]*ExperienceBreakpointsRecord
// ExperienceMaxLevels defines the max character levels // ExperienceMaxLevels defines the max character levels
type ExperienceMaxLevels map[d2enum.Hero]int type ExperienceMaxLevels map[d2enum.Hero]int
// ExperienceBreakpointRecord describes the experience points required to // ExperienceBreakpointsRecord describes the experience points required to
// gain a level for all character classes // gain a level for all character classes
type ExperienceBreakpointRecord struct { type ExperienceBreakpointsRecord 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.Debugf("Loaded %d Gamble records", len(records)) r.Logger.Infof("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]*GemRecord // LoadGems loads gem records into a map[string]*GemsRecord
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 := &GemRecord{ gem := &GemsRecord{
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.Debugf("Loaded %d Gem records", len(records)) r.Logger.Infof("Loaded %d Gems 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 GemRecords // Gems stores all of the GemsRecords
type Gems map[string]*GemRecord type Gems map[string]*GemsRecord
// GemRecord is a representation of a single row of gems.txt // GemsRecord 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 GemRecord struct { type GemsRecord 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.Debugf("Loaded %d HirelingDescription records", len(records)) r.Logger.Infof("Loaded %d Hireling Descriptions 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.Debugf("Loaded %d Hireling records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d HitClass records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d Inventory records", len(records)) r.Logger.Infof("Loaded %d Inventory Panel 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.Debugf("Loaded %d %s records", len(records), name) r.Logger.Infof("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.Debugf("Loaded %d Armor Item records", len(records)) r.Logger.Infof("Loaded %d armors", 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.Debugf("Loaded %d LowQuality records", len(records)) r.Logger.Infof("Loaded %d Low Item Quality 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.Debugf("Loaded %d Misc Item records", len(records)) r.Logger.Infof("Loaded %d misc items", 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.Debugf("Loaded %d ItemQuality records", len(records)) r.Logger.Infof("Loaded %d ItemQualities 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.Debugf("Loaded %d ItemRatio records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d ItemType records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d Weapon records", len(records)) r.Logger.Infof("Loaded %d weapons", 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.Debugf("Loaded %d ItemStatCost records", len(records)) r.Logger.Infof("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 := &LevelDetailRecord{ record := &LevelDetailsRecord{
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.Debugf("Loaded %d LevelDetail records", len(records)) r.Logger.Infof("Loaded %d LevelDetails 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 LevelDetailRecords // LevelDetails has all of the LevelDetailsRecords
type LevelDetails map[int]*LevelDetailRecord type LevelDetails map[int]*LevelDetailsRecord
// LevelDetailRecord is a representation of a row from levels.txt // LevelDetailsRecord 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 LevelDetailRecord struct { type LevelDetailsRecord 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 := &LevelMazeDetailRecord{ record := &LevelMazeDetailsRecord{
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.Debugf("Loaded %d LevelMazeDetail records", len(records)) r.Logger.Infof("Loaded %d LevelMazeDetails 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 LevelMazeDetailRecords // LevelMazeDetails stores all of the LevelMazeDetailsRecords
type LevelMazeDetails map[int]*LevelMazeDetailRecord type LevelMazeDetails map[int]*LevelMazeDetailsRecord
// LevelMazeDetailRecord is a representation of a row from lvlmaze.txt // LevelMazeDetailsRecord 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 LevelMazeDetailRecord struct { type LevelMazeDetailsRecord 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.Debugf("Loaded %d LevelPresets records", len(records)) r.Logger.Infof("Loaded %d level presets", 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.Debugf("Loaded %d LevelSubstitution records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d LevelType records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d LevelWarp records", len(records)) r.Logger.Infof("Loaded %d level warps", 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.Debugf("Loaded %d Missile records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d MonsterAI records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d MonsterEquipment records", length) r.Logger.Infof("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.Debugf("Loaded %d MonsterLevel records", len(records)) r.Logger.Infof("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.Debugf("Loaded %d MonMode records", len(records)) r.Logger.Infof("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 MonModeRecords // MonModes stores all of the GemsRecords
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