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
"on": [pull_request]
jobs:
build:
name: ''
runs-on: self-hosted
name: Build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- 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
At the moment (december 2020) the game starts, you can select any character and run around Act1 town.
You can also open any of the game's panels.
At the moment (october 2020) the game starts, you can select any character and run around Act1 town.
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)
![Game Panels](docs/game_panels.png)
## Additional Credits
- Diablo2 Logo

View File

@ -6,7 +6,6 @@ import (
"container/ring"
"encoding/json"
"errors"
"flag"
"fmt"
"image"
"image/gif"
@ -25,6 +24,7 @@ import (
"github.com/pkg/profile"
"golang.org/x/image/colornames"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"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
type Options struct {
printVersion *bool
Debug *bool
profiler *string
Server *d2networking.ServerOptions
LogLevel *d2util.LogLevel
}
type bindTerminalEntry struct {
name string
description string
action interface{}
}
const (
bytesToMegabyte = 1024 * 1024
nSamplesTAlloc = 100
@ -103,24 +110,21 @@ const (
// Create creates a new instance of the application
func Create(gitBranch, gitCommit string) *App {
logger := d2util.NewLogger()
logger.SetPrefix(appLoggerPrefix)
assetManager, assetError := d2asset.NewAssetManager()
app := &App{
Logger: logger,
gitBranch: gitBranch,
gitCommit: gitCommit,
asset: assetManager,
Options: &Options{
Server: &d2networking.ServerOptions{},
},
errorMessage: assetError,
}
app.Infof("OpenDiablo2 - Open source Diablo 2 engine")
app.parseArguments()
app.SetLevel(*app.Options.LogLevel)
app.asset, app.errorMessage = d2asset.NewAssetManager(*app.Options.LogLevel)
app.Logger = d2util.NewLogger()
app.Logger.SetPrefix(appLoggerPrefix)
app.Logger.SetLevel(d2util.LogLevelNone)
return app
}
@ -136,7 +140,7 @@ func (a *App) startDedicatedServer() error {
srvChanIn := make(chan int)
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 {
return srvErr
}
@ -169,7 +173,15 @@ func (a *App) loadEngine() error {
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()
@ -178,9 +190,14 @@ func (a *App) loadEngine() error {
return err
}
err = a.asset.BindTerminalCommands(term)
if err != nil {
return err
}
scriptEngine := d2script.CreateScriptEngine()
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio)
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, a.config.LogLevel, audio)
a.inputManager = inputManager
a.terminal = term
@ -189,48 +206,50 @@ func (a *App) loadEngine() error {
a.ui = uiManager
a.tAllocSamples = createZeroedRing(nSamplesTAlloc)
if a.gitBranch == "" {
a.gitBranch = "Local Build"
}
return nil
}
func (a *App) parseArguments() {
const (
descProfile = "Profiles the program,\none of (cpu, mem, block, goroutine, trace, thread, mutex)"
descPlayers = "Sets the number of max players for the dedicated server"
descLogging = "Enables verbose logging. Log levels will include those below it.\n" +
" 0 disables log messages\n" +
" 1 shows fatal\n" +
" 2 shows error\n" +
" 3 shows warning\n" +
" 4 shows info\n" +
" 5 shows debug\n"
versionArg = "version"
versionShort = 'v'
versionDesc = "Prints the version of the app"
profilerArg = "profile"
profilerDesc = "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)"
serverArg = "dedicated"
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.Server.Dedicated = flag.Bool("dedicated", false, "Starts a dedicated server")
a.Options.Server.MaxPlayers = flag.Int("players", 0, descPlayers)
a.Options.LogLevel = flag.Int("l", d2util.LogLevelDefault, descLogging)
showVersion := flag.Bool("v", false, "Show version")
showHelp := flag.Bool("h", false, "Show help")
a.Options.profiler = kingpin.Flag(profilerArg, profilerDesc).String()
a.Options.Server.Dedicated = kingpin.Flag(serverArg, serverDesc).Short(serverShort).Bool()
a.Options.printVersion = kingpin.Flag(versionArg, versionDesc).Short(versionShort).Bool()
a.Options.Server.MaxPlayers = kingpin.Flag(playersArg, playersDesc).Int()
a.Options.LogLevel = kingpin.Flag(loggingArg, loggingDesc).
Short(loggingShort).
Default(strconv.Itoa(d2util.LogLevelUnspecified)).
Int()
flag.Usage = func() {
fmt.Printf("usage: %s [<flags>]\n\nFlags:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if *a.Options.LogLevel >= d2util.LogLevelUnspecified {
*a.Options.LogLevel = d2util.LogLevelDefault
}
if *showVersion {
a.Infof("version: OpenDiablo2 (%s %s)", a.gitBranch, a.gitCommit)
os.Exit(0)
}
if *showHelp {
flag.Usage()
os.Exit(0)
}
kingpin.Parse()
}
// 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
func (a *App) Run() (err error) {
func (a *App) Run() error {
a.parseArguments()
// add our possible config directories
_, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()))
_, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()))
if a.config, err = a.LoadConfig(); err != nil {
config, err := a.LoadConfig()
if err != nil {
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
if len(*a.Options.profiler) > 0 {
profiler := enableProfiler(*a.Options.profiler, a)
@ -340,39 +389,36 @@ func (a *App) initialize() error {
a.renderer.SetWindowIcon("d2logo.png")
a.terminal.BindLogger()
terminalCommands := []struct {
name string
desc string
args []string
fn func(args []string) error
}{
{"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap},
{"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen},
{"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame},
{"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture},
{"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture},
{"vsync", "toggles vsync", nil, a.toggleVsync},
{"fps", "toggle fps counter", nil, a.toggleFpsCounter},
{"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale},
{"quit", "exits the game", nil, a.quitGame},
{"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground},
{"js", "eval JS scripts", []string{"code"}, a.evalJS},
terminalActions := [...]bindTerminalEntry{
{"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap},
{"fullscreen", "toggles fullscreen", a.toggleFullScreen},
{"capframe", "captures a still frame", a.setupCaptureFrame},
{"capgifstart", "captures an animation (start)", a.startAnimationCapture},
{"capgifstop", "captures an animation (stop)", a.stopAnimationCapture},
{"vsync", "toggles vsync", a.toggleVsync},
{"fps", "toggle fps counter", a.toggleFpsCounter},
{"timescale", "set scalar for elapsed time", a.setTimeScale},
{"quit", "exits the game", a.quitGame},
{"screen-gui", "enters the gui playground screen", a.enterGuiPlayground},
{"js", "eval JS scripts", a.evalJS},
}
for _, cmd := range terminalCommands {
if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil {
a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error())
for idx := range terminalActions {
action := &terminalActions[idx]
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 {
return err
}
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)
@ -636,7 +682,7 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
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.Mkdir("./pprof/", 0750); err != nil {
a.Fatal(err.Error())
@ -655,56 +701,48 @@ func (a *App) dumpHeap([]string) error {
if err := fileOut.Close(); err != nil {
a.Fatal(err.Error())
}
return nil
}
func (a *App) evalJS(args []string) error {
val, err := a.scriptEngine.Eval(args[0])
func (a *App) evalJS(code string) {
val, err := a.scriptEngine.Eval(code)
if err != nil {
a.terminal.Errorf(err.Error())
return nil
a.terminal.OutputErrorf("%s", err)
return
}
a.Info("%s" + val)
return nil
}
func (a *App) toggleFullScreen([]string) error {
func (a *App) toggleFullScreen() {
fullscreen := !a.renderer.IsFullScreen()
a.renderer.SetFullScreen(fullscreen)
a.terminal.Infof("fullscreen is now: %v", fullscreen)
return nil
a.terminal.OutputInfof("fullscreen is now: %v", fullscreen)
}
func (a *App) setupCaptureFrame(args []string) error {
func (a *App) setupCaptureFrame(path string) {
a.captureState = captureStateFrame
a.capturePath = args[0]
a.capturePath = path
a.captureFrames = nil
return nil
}
func (a *App) doCaptureFrame(target d2interface.Surface) error {
fp, err := os.Create(a.capturePath)
if err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return err
}
defer func() {
if err := fp.Close(); err != nil {
a.Fatal(err.Error())
}
}()
screenshot := target.Screenshot()
if err := png.Encode(fp, screenshot); err != nil {
return err
}
if err := fp.Close(); err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return nil
}
a.terminal.Infof("saved frame to %s", a.capturePath)
a.Info(fmt.Sprintf("saved frame to %s", a.capturePath))
return nil
}
@ -764,61 +802,47 @@ func (a *App) convertFramesToGif() error {
return err
}
a.Infof("saved animation to %s", a.capturePath)
a.Info(fmt.Sprintf("saved animation to %s", a.capturePath))
return nil
}
func (a *App) startAnimationCapture(args []string) error {
func (a *App) startAnimationCapture(path string) {
a.captureState = captureStateGif
a.capturePath = args[0]
a.capturePath = path
a.captureFrames = nil
return nil
}
func (a *App) stopAnimationCapture([]string) error {
func (a *App) stopAnimationCapture() {
a.captureState = captureStateNone
return nil
}
func (a *App) toggleVsync([]string) error {
func (a *App) toggleVsync() {
vsync := !a.renderer.GetVSyncEnabled()
a.renderer.SetVSyncEnabled(vsync)
a.terminal.Infof("vsync is now: %v", vsync)
return nil
a.terminal.OutputInfof("vsync is now: %v", vsync)
}
func (a *App) toggleFpsCounter([]string) error {
func (a *App) toggleFpsCounter() {
a.showFPS = !a.showFPS
a.terminal.Infof("fps counter is now: %v", a.showFPS)
return nil
a.terminal.OutputInfof("fps counter is now: %v", a.showFPS)
}
func (a *App) setTimeScale(args []string) error {
timeScale, err := strconv.ParseFloat(args[0], 64)
if err != nil || timeScale <= 0 {
a.terminal.Errorf("invalid time scale value")
return nil
}
a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale)
func (a *App) setTimeScale(timeScale float64) {
if timeScale <= 0 {
a.terminal.OutputErrorf("invalid time scale value")
} else {
a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale)
a.timeScale = timeScale
return nil
}
}
func (a *App) quitGame([]string) error {
func (a *App) quitGame() {
os.Exit(0)
return nil
}
func (a *App) enterGuiPlayground([]string) error {
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset))
return nil
func (a *App) enterGuiPlayground() {
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, a.config.LogLevel, a.asset))
}
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}
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 {
a.Error(err.Error())
return
@ -898,7 +922,7 @@ func (a *App) ToMainMenu(errorMessageOptional ...string) {
// ToSelectHero forces the game to transition to the Select Hero (create character) screen
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, *a.Options.LogLevel, host)
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, a.config.LogLevel, host)
if err != nil {
a.Error(err.Error())
return
@ -909,18 +933,18 @@ func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType,
// ToCreateGame forces the game to transition to the Create Game screen
func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) {
gameClient, err := d2client.Create(connType, a.asset, *a.Options.LogLevel, a.scriptEngine)
gameClient, err := d2client.Create(connType, a.asset, a.config.LogLevel, a.scriptEngine)
if err != nil {
a.Error(err.Error())
}
if err = gameClient.Open(host, filePath); err != nil {
errorMessage := fmt.Sprintf("can not connect to the host: %s", host)
a.Error(errorMessage)
fmt.Println(errorMessage)
a.ToMainMenu(errorMessage)
} else {
game, err := d2gamescreen.CreateGame(
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, *a.Options.LogLevel, a.guiManager,
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, a.config.LogLevel, a.guiManager,
)
if err != nil {
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
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
a.audio, a.ui, connType, *a.Options.LogLevel, connHost)
a.audio, a.ui, connType, a.config.LogLevel, connHost)
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)
@ -944,7 +968,7 @@ func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnection
// ToMapEngineTest forces the game to transition to the map engine test screen
func (a *App) ToMapEngineTest(region, level int) {
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio,
*a.Options.LogLevel, a.screen)
a.config.LogLevel, a.screen)
if err != nil {
a.Error(err.Error())
return
@ -955,10 +979,10 @@ func (a *App) ToMapEngineTest(region, level int) {
// ToCredits forces the game to transition to the credits screen
func (a *App) ToCredits() {
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, *a.Options.LogLevel, a.ui))
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
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"
)
const (
bytesPerInt16 = 2
bytesPerInt32 = 4
bytesPerInt64 = 8
)
// StreamReader allows you to read data from a byte array in various formats
type StreamReader struct {
data []byte
@ -20,6 +26,16 @@ func CreateStreamReader(source []byte) *StreamReader {
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
func (v *StreamReader) GetByte() byte {
result := v.data[v.position]
@ -28,46 +44,32 @@ func (v *StreamReader) GetByte() byte {
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
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
//nolint
func (v *StreamReader) GetUInt16() uint16 {
b := v.ReadBytes(2)
return uint16(b[0]) | uint16(b[1])<<8
}
v.position += bytesPerInt16
// GetInt32 returns an int32 dword from the stream
func (v *StreamReader) GetInt32() int32 {
return int32(v.GetUInt32())
}
// 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
return result
}
// SetPosition sets the stream position with the given position
@ -75,9 +77,51 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
v.position = newPosition
}
// GetSize returns the total size of the stream in bytes
func (v *StreamReader) GetSize() uint64 {
return uint64(len(v.data))
// GetUInt32 returns a uint32 dword from the stream
func (v *StreamReader) GetUInt32() uint32 {
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

View File

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

View File

@ -2,9 +2,7 @@ package d2enum
// there are labels for "numeric labels (see AssetManager.TranslateLabel)
const (
RepairAll = iota
_
CancelLabel
CancelLabel = iota
CopyrightLabel
AllRightsReservedLabel
SinglePlayerLabel
@ -64,8 +62,6 @@ const (
// BaseLabelNumbers returns base label value (#n in english string table table)
func BaseLabelNumbers(idx int) int {
baseLabelNumbers := []int{
128, // repairAll
127,
// main menu labels
1612, // CANCEL
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 (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
@ -20,9 +21,31 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
type MPQ struct {
filePath string
file *os.File
hashes map[uint64]*Hash
blocks []*Block
header Header
hashEntryMap HashEntryMap
blockTableEntries []BlockTableEntry
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.
@ -30,153 +53,71 @@ type PatchInfo struct {
Length uint32 // Length of patch info header, in bytes
Flags uint32 // Flags. 0x80000000 = MD5 (?)
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
func New(fileName string) (*MPQ, error) {
mpq := &MPQ{filePath: fileName}
// 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
)
// 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
if runtime.GOOS == "linux" {
mpq.file, err = openIgnoreCase(fileName)
result.file, err = openIgnoreCase(fileName)
} 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 {
return nil, err
}
if err := mpq.readHeader(); err != nil {
return nil, fmt.Errorf("failed to read reader: %v", err)
}
return mpq, nil
}
// FromFile loads an MPQ file and returns a MPQ structure
func FromFile(fileName string) (*MPQ, error) {
mpq, err := New(fileName)
if err != nil {
if err := result.readHeader(); err != nil {
return nil, err
}
if err := mpq.readHashTable(); err != nil {
return nil, fmt.Errorf("failed to read hash table: %v", err)
}
if err := mpq.readBlockTable(); err != nil {
return nil, fmt.Errorf("failed to read block table: %v", err)
}
return mpq, nil
}
// getFileBlockData gets a block table entry
func (mpq *MPQ) getFileBlockData(fileName string) (*Block, error) {
fileEntry, ok := mpq.hashes[hashFilename(fileName)]
if !ok {
return nil, errors.New("file not found")
}
if fileEntry.BlockIndex >= uint32(len(mpq.blocks)) {
return nil, errors.New("invalid block index")
}
return mpq.blocks[fileEntry.BlockIndex], nil
}
// Close closes the MPQ file
func (mpq *MPQ) Close() error {
return mpq.file.Close()
}
// ReadFile reads a file from the MPQ and returns a memory stream
func (mpq *MPQ) ReadFile(fileName string) ([]byte, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return []byte{}, err
}
fileBlockData.FileName = strings.ToLower(fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return []byte{}, err
}
buffer := make([]byte, fileBlockData.UncompressedFileSize)
if _, err := stream.Read(buffer, 0, fileBlockData.UncompressedFileSize); err != nil {
return []byte{}, err
}
return buffer, nil
}
// ReadFileStream reads the mpq file data and returns a stream
func (mpq *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
fileBlockData, err := mpq.getFileBlockData(fileName)
if err != nil {
return nil, err
}
fileBlockData.FileName = strings.ToLower(fileName)
stream, err := CreateStream(mpq, fileBlockData, fileName)
if err != nil {
return nil, err
}
return &MpqDataStream{stream: stream}, nil
}
// ReadTextFile reads a file and returns it as a string
func (mpq *MPQ) ReadTextFile(fileName string) (string, error) {
data, err := mpq.ReadFile(fileName)
if err != nil {
return "", err
}
return string(data), nil
}
// Listfile returns the list of files in this MPQ
func (mpq *MPQ) Listfile() ([]string, error) {
data, err := mpq.ReadFile("(listfile)")
if err != nil {
return nil, err
}
raw := strings.TrimRight(string(data), "\x00")
s := bufio.NewScanner(strings.NewReader(raw))
var filePaths []string
for s.Scan() {
filePath := s.Text()
filePaths = append(filePaths, filePath)
}
return filePaths, nil
}
// Path returns the MPQ file path
func (mpq *MPQ) Path() string {
return mpq.filePath
}
// Contains returns bool for whether the given filename exists in the mpq
func (mpq *MPQ) Contains(filename string) bool {
_, ok := mpq.hashes[hashFilename(filename)]
return ok
}
// Size returns the size of the mpq in bytes
func (mpq *MPQ) Size() uint32 {
return mpq.header.ArchiveSize
return result, nil
}
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
func (m *MpqDataStream) Read(p []byte) (n int, err error) {
totalRead, err := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), err
totalRead := m.stream.Read(p, 0, uint32(len(p)))
return int(totalRead), nil
}
// Seek sets the position of the data stream
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
m.stream.Position = uint32(offset + int64(whence))
return int64(m.stream.Position), nil
m.stream.CurrentPosition = uint32(offset + int64(whence))
return int64(m.stream.CurrentPosition), nil
}
// 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"
"errors"
"fmt"
"io"
"log"
"strings"
"github.com/JoshVarga/blast"
@ -16,63 +17,80 @@ import (
// Stream represents a stream of data in an MPQ archive
type Stream struct {
Data []byte
Positions []uint32
MPQ *MPQ
Block *Block
Index uint32
Size uint32
Position uint32
BlockTableEntry BlockTableEntry
BlockPositions []uint32
CurrentData []byte
FileName string
MPQData *MPQ
EncryptionSeed uint32
CurrentPosition uint32
CurrentBlockIndex uint32
BlockSize uint32
}
// CreateStream creates an MPQ stream
func CreateStream(mpq *MPQ, block *Block, fileName string) (*Stream, error) {
s := &Stream{
MPQ: mpq,
Block: block,
Index: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
func CreateStream(mpq *MPQ, blockTableEntry BlockTableEntry, fileName string) (*Stream, error) {
result := &Stream{
MPQData: mpq,
BlockTableEntry: blockTableEntry,
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) {
s.Block.calculateEncryptionSeed(fileName)
result.BlockSize = 0x200 << result.MPQData.data.BlockSize //nolint:gomnd // MPQ magic
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) {
return nil, errors.New("patching is not supported")
if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) &&
!result.BlockTableEntry.HasFlag(FileSingleUnit) {
err = result.loadBlockOffsets()
}
if (s.Block.HasFlag(FileCompress) || s.Block.HasFlag(FileImplode)) && !s.Block.HasFlag(FileSingleUnit) {
if err := s.loadBlockOffsets(); err != nil {
return nil, err
}
}
return s, nil
return result, err
}
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
}
blockPositionCount := ((v.Block.UncompressedFileSize + v.Size - 1) / v.Size) + 1
v.Positions = make([]uint32, blockPositionCount)
mpqBytes := make([]byte, blockPositionCount*4) //nolint:gomnd // MPQ magic
if err := binary.Read(v.MPQ.file, binary.LittleEndian, &v.Positions); err != nil {
_, err = v.MPQData.file.Read(mpqBytes)
if err != nil {
return err
}
if v.Block.HasFlag(FileEncrypted) {
decrypt(v.Positions, v.Block.EncryptionSeed-1)
for i := range v.BlockPositions {
idx := i * 4 //nolint:gomnd // MPQ magic
v.BlockPositions[i] = binary.LittleEndian.Uint32(mpqBytes[idx : idx+4])
}
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")
}
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")
}
}
@ -80,18 +98,16 @@ func (v *Stream) loadBlockOffsets() error {
return nil
}
func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, err error) {
if v.Block.HasFlag(FileSingleUnit) {
func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
if v.BlockTableEntry.HasFlag(FileSingleUnit) {
return v.readInternalSingleUnit(buffer, offset, count)
}
var read uint32
toRead := count
readTotal := uint32(0)
for toRead > 0 {
if read, err = v.readInternal(buffer, offset, toRead); err != nil {
return 0, err
}
read := v.readInternal(buffer, offset, toRead)
if read == 0 {
break
@ -102,153 +118,149 @@ func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, er
toRead -= read
}
return readTotal, nil
return readTotal
}
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) (uint32, error) {
if len(v.Data) == 0 {
if err := v.loadSingleUnit(); err != nil {
return 0, err
}
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) uint32 {
if len(v.CurrentData) == 0 {
v.loadSingleUnit()
}
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) {
if err := v.bufferData(); err != nil {
return 0, err
}
func (v *Stream) readInternal(buffer []byte, offset, count uint32) uint32 {
v.bufferData()
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 {
return 0, nil
return 0
}
copy(buffer[offset:offset+bytesToCopy], v.Data[pos:pos+bytesToCopy])
v.Position += bytesToCopy
copy(buffer[offset:offset+uint32(bytesToCopy)], v.CurrentData[localPosition:localPosition+uint32(bytesToCopy)])
return bytesToCopy, nil
v.CurrentPosition += uint32(bytesToCopy)
return uint32(bytesToCopy)
}
func (v *Stream) bufferData() (err error) {
blockIndex := v.Position / v.Size
func (v *Stream) bufferData() {
requiredBlock := v.CurrentPosition / v.BlockSize
if blockIndex == v.Index {
return nil
if requiredBlock == v.CurrentBlockIndex {
return
}
expectedLength := d2math.Min(v.Block.UncompressedFileSize-(blockIndex*v.Size), v.Size)
if v.Data, err = v.loadBlock(blockIndex, expectedLength); err != nil {
return err
expectedLength := d2math.Min(v.BlockTableEntry.UncompressedFileSize-(requiredBlock*v.BlockSize), v.BlockSize)
v.CurrentData = v.loadBlock(requiredBlock, expectedLength)
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) {
if _, err = v.MPQ.file.Seek(int64(v.MPQ.header.HeaderSize), io.SeekStart); err != nil {
return err
_, err = v.MPQData.file.Read(fileData)
if err != nil {
log.Print(err)
}
fileData := make([]byte, v.Size)
if _, err = v.MPQ.file.Read(fileData); err != nil {
return err
if v.BlockSize == v.BlockTableEntry.UncompressedFileSize {
v.CurrentData = fileData
return
}
if v.Size == v.Block.UncompressedFileSize {
v.Data = fileData
return nil
v.CurrentData = decompressMulti(fileData, v.BlockTableEntry.UncompressedFileSize)
}
v.Data, err = decompressMulti(fileData, v.Block.UncompressedFileSize)
return err
}
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) ([]byte, error) {
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte {
var (
offset uint32
toRead uint32
)
if v.Block.HasFlag(FileCompress) || v.Block.HasFlag(FileImplode) {
offset = v.Positions[blockIndex]
toRead = v.Positions[blockIndex+1] - offset
if v.BlockTableEntry.HasFlag(FileCompress) || v.BlockTableEntry.HasFlag(FileImplode) {
offset = v.BlockPositions[blockIndex]
toRead = v.BlockPositions[blockIndex+1] - offset
} else {
offset = blockIndex * v.Size
offset = blockIndex * v.BlockSize
toRead = expectedLength
}
offset += v.Block.FilePosition
offset += v.BlockTableEntry.FilePosition
data := make([]byte, toRead)
if _, err := v.MPQ.file.Seek(int64(offset), io.SeekStart); err != nil {
return []byte{}, err
_, err := v.MPQData.file.Seek(int64(offset), 0)
if err != nil {
log.Print(err)
}
if _, err := v.MPQ.file.Read(data); err != nil {
return []byte{}, err
_, err = v.MPQData.file.Read(data)
if err != nil {
log.Print(err)
}
if v.Block.HasFlag(FileEncrypted) && v.Block.UncompressedFileSize > 3 {
if v.Block.EncryptionSeed == 0 {
return []byte{}, errors.New("unable to determine encryption key")
if v.BlockTableEntry.HasFlag(FileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 {
if v.EncryptionSeed == 0 {
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.Block.HasFlag(FileSingleUnit) {
return decompressMulti(data, expectedLength)
if v.BlockTableEntry.HasFlag(FileCompress) && (toRead != expectedLength) {
if !v.BlockTableEntry.HasFlag(FileSingleUnit) {
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 pkDecompress(data)
}
return data, nil
return data
}
//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]
switch compressionType {
case 1: // Huffman
return []byte{}, errors.New("huffman decompression not supported")
panic("huffman decompression not supported")
case 2: // ZLib/Deflate
return deflate(data[1:])
case 8: // PKLib/Impode
return pkDecompress(data[1:])
case 0x10: // BZip2
return []byte{}, errors.New("bzip2 decompression not supported")
panic("bzip2 decompression not supported")
case 0x80: // IMA ADPCM Stereo
return d2compression.WavDecompress(data[1:], 2), nil
return d2compression.WavDecompress(data[1:], 2)
case 0x40: // IMA ADPCM Mono
return d2compression.WavDecompress(data[1:], 1), nil
return d2compression.WavDecompress(data[1:], 1)
case 0x12:
return []byte{}, errors.New("lzma decompression not supported")
panic("lzma decompression not supported")
// Combos
case 0x22:
// sparse then zlib
return []byte{}, errors.New("sparse decompression + deflate decompression not supported")
panic("sparse decompression + deflate decompression not supported")
case 0x30:
// sparse then bzip2
return []byte{}, errors.New("sparse decompression + bzip2 decompression not supported")
panic("sparse decompression + bzip2 decompression not supported")
case 0x41:
sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 1)
@ -256,68 +268,69 @@ func decompressMulti(data []byte /*expectedLength*/, _ uint32) ([]byte, error) {
copy(tmp, sinput)
return tmp, nil
return tmp
case 0x48:
// byte[] result = PKDecompress(sinput, outputLength);
// return MpqWavCompression.Decompress(new MemoryStream(result), 1);
return []byte{}, errors.New("pk + mpqwav decompression not supported")
panic("pk + mpqwav decompression not supported")
case 0x81:
sinput := d2compression.HuffmanDecompress(data[1:])
sinput = d2compression.WavDecompress(sinput, 2)
tmp := make([]byte, len(sinput))
copy(tmp, sinput)
return tmp, nil
return tmp
case 0x88:
// byte[] result = PKDecompress(sinput, outputLength);
// 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, error) {
func deflate(data []byte) []byte {
b := bytes.NewReader(data)
r, err := zlib.NewReader(b)
if err != nil {
return []byte{}, err
panic(err)
}
buffer := new(bytes.Buffer)
_, err = buffer.ReadFrom(r)
if err != nil {
return []byte{}, err
log.Panic(err)
}
err = r.Close()
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)
r, err := blast.NewReader(b)
if err != nil {
return []byte{}, err
panic(err)
}
buffer := new(bytes.Buffer)
if _, err = buffer.ReadFrom(r); err != nil {
return []byte{}, err
_, err = buffer.ReadFrom(r)
if err != nil {
panic(err)
}
err = r.Close()
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
Contains(string) bool
Size() uint32
Close() error
Close()
FileExists(fileName string) bool
ReadFile(fileName string) ([]byte, error)
ReadFileStream(fileName string) (DataStream, error)
ReadTextFile(fileName string) (string, error)
Listfile() ([]string, error)
GetFileList() ([]string, error)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -62,7 +62,7 @@ key | value key | value
So, GetLabelModifier returns value of offset in locale languages table
*/
// 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 {
modifiers := map[string]int{
"ENG": 0, // (English) // checked
@ -70,7 +70,7 @@ func GetLabelModifier(language string) int {
"DEU": 0, // (German) // checked
"FRA": 0, // (French)
"POR": 0, // (Portuguese)
"ITA": 0, // (Italian) // checked
"ITA": 0, // (Italian)
"JPN": 0, // (Japanese)
"KOR": 0, // (Korean)
"SIN": 0, //

View File

@ -193,7 +193,6 @@ const (
QuestLogQDescrBtn = "/data/global/ui/MENU/questlast.dc6"
QuestLogSocket = "/data/global/ui/MENU/questsockets.dc6"
QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6"
QuestLogDoneSfx = "cursor/questdone.wav"
// --- Mouse Pointers ---
@ -246,8 +245,6 @@ const (
Frame = "/data/global/ui/PANEL/800borderframe.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"
SkillsPanelAmazon = "/data/global/ui/SPELLS/skltree_a_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
func (a *Sprite) SetCurrentFrame(frameIndex int) error {
if frameIndex >= a.GetFrameCount() || frameIndex < 0 {
if frameIndex >= a.GetFrameCount() {
return errors.New("invalid frame index")
}

View File

@ -37,16 +37,16 @@ type GlyphPrinter struct {
// Basic Latin and C1 Controls and Latin-1 Supplement.
//
// DebugPrint always returns nil as of 1.5.0-alpha.
func (p *GlyphPrinter) Print(target interface{}, str string) error {
p.PrintAt(target.(*ebiten.Image), str, 0, 0)
func (p *GlyphPrinter) Print(target *ebiten.Image, str string) error {
p.PrintAt(target, str, 0, 0)
return nil
}
// 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
// Basic Latin and C1 Controls and Latin-1 Supplement.
func (p *GlyphPrinter) PrintAt(target interface{}, str string, x, y int) {
p.drawDebugText(target.(*ebiten.Image), str, x, y, false)
func (p *GlyphPrinter) PrintAt(target *ebiten.Image, str string, x, y int) {
p.drawDebugText(target, str, x, y, false)
}
func (p *GlyphPrinter) drawDebugText(target *ebiten.Image, str string, ox, oy int, shadow bool) {

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,9 @@ package d2asset
import (
"errors"
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
@ -129,11 +132,24 @@ func (a *DCCAnimation) decodeDirection(directionIndex int) error {
func (a *DCCAnimation) decodeFrame(directionIndex int) animationFrame {
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{
width: dccDirection.Box.Width,
height: dccDirection.Box.Height,
offsetX: dccDirection.Box.Left,
offsetY: dccDirection.Box.Top,
width: frameWidth,
height: frameHeight,
offsetX: minX,
offsetY: minY,
decoded: true,
}

View File

@ -3,7 +3,6 @@ package d2audio
import (
"fmt"
"math/rand"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
@ -32,7 +31,7 @@ const originalFPS float64 = 25
// A Sound that can be started and stopped
type Sound struct {
effect d2interface.SoundEffect
entry *d2records.SoundDetailRecord
entry *d2records.SoundDetailsRecord
volume float64
vTarget 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
type SoundEngine struct {
asset *d2asset.AssetManager
@ -134,25 +128,43 @@ func NewSoundEngine(provider d2interface.AudioProvider,
r.Logger.SetPrefix(logPrefix)
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())
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())
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())
return nil
return
}
if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil {
r.Error(err.Error())
return nil
r.Info(fmt.Sprint(s))
}
})
err = term.BindAction("killsounds", "kill active sounds", func() {
for s := range r.sounds {
if err != nil {
r.Error(err.Error())
return
}
s.Stop()
}
})
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
func (s *SoundEngine) Reset() {
for snd := range s.sounds {
@ -235,35 +242,3 @@ func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
sound := s.asset.Records.Sound.Details[handle].Index
return s.PlaySoundID(sound)
}
func (s *SoundEngine) commandPlaySoundID(args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument")
}
s.PlaySoundID(id)
return nil
}
func (s *SoundEngine) commandPlaySound(args []string) error {
s.PlaySoundHandle(args[0])
return nil
}
func (s *SoundEngine) commandActiveSounds([]string) error {
for sound := range s.sounds {
s.Info(sound.String())
}
return nil
}
func (s *SoundEngine) commandKillSounds([]string) error {
for sound := range s.sounds {
sound.Stop()
}
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
package d2gui
import (
"image/color"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
)
@ -35,3 +37,28 @@ func renderSegmented(animation d2interface.Animation, segmentsX, segmentsY, fram
func half(n int) int {
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)
defer target.Pop()
drawColor := d2util.Color(white)
drawColor := rgbaColor(white)
switch entry.widget.(type) {
case *Layout:
drawColor = d2util.Color(magenta)
drawColor = rgbaColor(magenta)
case *SpacerStatic, *SpacerDynamic:
drawColor = d2util.Color(grey2)
drawColor = rgbaColor(grey2)
case *Label:
drawColor = d2util.Color(green)
drawColor = rgbaColor(green)
case *Button:
drawColor = d2util.Color(yellow)
drawColor = rgbaColor(yellow)
}
target.DrawLine(entry.width, 0, drawColor)
@ -487,7 +487,7 @@ func (l *Layout) createButton(renderer d2interface.Renderer, text string,
return nil, loadErr
}
textColor := d2util.Color(grey)
textColor := rgbaColor(grey)
textWidth, textHeight := font.GetTextMetrics(text)
textX := half(buttonWidth) - half(textWidth)
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
func NewLayoutScrollbar(parentLayout, targetLayout *Layout) *LayoutScrollbar {
func NewLayoutScrollbar(
parentLayout *Layout,
targetLayout *Layout,
) *LayoutScrollbar {
parentW, parentH := parentLayout.GetSize()
_, targetH := targetLayout.GetSize()
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.
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{}
for idx := range classStats.BaseSkill {

View File

@ -10,27 +10,32 @@ type HeroStatsState struct {
Level int `json:"level"`
Experience int `json:"experience"`
Strength int `json:"strength"`
Energy int `json:"energy"`
Dexterity int `json:"dexterity"`
Vitality int `json:"vitality"`
// there are stats and skills points remaining to add.
StatsPoints int `json:"statsPoints"`
SkillPoints int `json:"skillPoints"`
Energy int `json:"energy"`
Strength int `json:"strength"`
Dexterity int `json:"dexterity"`
AttackRating int `json:"attackRating"`
DefenseRating int `json:"defenseRating"`
MaxStamina int `json:"maxStamina"`
Health int `json:"health"`
MaxHealth int `json:"maxHealth"`
Mana int `json:"mana"`
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)
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
NextLevelExp int `json:"-"`
}
// 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{
Level: 1,
Experience: 0,
@ -39,8 +44,6 @@ func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStat
Dexterity: classStats.InitDex,
Vitality: classStats.InitVit,
Energy: classStats.InitEne,
StatsPoints: 0,
SkillPoints: 0,
MaxHealth: classStats.InitVit * classStats.LifePerVit,
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
func (f *InventoryItemFactory) GetWeaponItemByCode(code string) (*InventoryItemWeapon, error) {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/796
result := f.asset.Records.Item.Weapons[code]
if result == nil {
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
var charStats = map[d2enum.Hero]*d2records.CharStatRecord{
var charStats = map[d2enum.Hero]*d2records.CharStatsRecord{
d2enum.HeroPaladin: {
Class: d2enum.HeroPaladin,
SkillStrAll: "to Paladin Skill Levels",
@ -297,7 +297,7 @@ var skillDetails = map[int]*d2records.SkillRecord{
}
// nolint:gochecknoglobals // just a test
var monStats = map[string]*d2records.MonStatRecord{
var monStats = map[string]*d2records.MonStatsRecord{
"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,
// sequence and tileType.
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 {
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.
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,
leftSkill, rightSkill, gold int) *Player {
leftSkill, rightSkill int, gold int) *Player {
layerEquipment := &[d2enum.CompositeTypeMax]string{
d2enum.CompositeTypeHead: equipment.Head.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.
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
result := &NPC{
mapEntity: newMapEntity(x, y),
@ -237,6 +237,7 @@ func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.Ove
return nil, err
}
// https://github.com/OpenDiablo2/OpenDiablo2/issues/767
animation.Rewind()
animation.ResetPlayedCount()
@ -262,7 +263,7 @@ func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.Ove
}
// 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) {
locX, locY := float64(x), float64(y)
entity := &Object{

View File

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

View File

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

View File

@ -2,10 +2,8 @@ package d2maprenderer
import (
"errors"
"fmt"
"image/color"
"math"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
@ -88,11 +86,20 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
result.Camera.position = &startPosition
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)
}
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)
}
@ -103,33 +110,6 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
return result
}
// UnbindTerminalCommands unbinds commands from the terminal
func (mr *MapRenderer) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("mapdebugvis", "entitydebugvis")
}
func (mr *MapRenderer) commandMapDebugVis(args []string) error {
level, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument supplied")
}
mr.mapDebugVisLevel = level
return nil
}
func (mr *MapRenderer) commandEntityDebugVis(args []string) error {
level, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument supplied")
}
mr.entityDebugVisLevel = level
return nil
}
// RegenerateTileCache calls MapRenderer.generateTileCache().
func (mr *MapRenderer) RegenerateTileCache() {
mr.generateTileCache()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,11 @@ package d2records
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// CharStats holds all of the CharStatRecords
type CharStats map[d2enum.Hero]*CharStatRecord
// CharStats holds all of the CharStatsRecords
type CharStats map[d2enum.Hero]*CharStatsRecord
// CharStatRecord is a struct that represents a single row from charstats.txt
type CharStatRecord struct {
// CharStatsRecord is a struct that represents a single row from charstats.txt
type CharStatsRecord struct {
Class d2enum.Hero
// 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.Debugf("Loaded %d Color records", len(records))
r.Logger.Infof("Loaded %d Color records", len(records))
return nil
}

View File

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

View File

@ -22,7 +22,7 @@ func compositeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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
}

View File

@ -22,7 +22,7 @@ func cubeModifierLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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
}

View File

@ -22,7 +22,7 @@ func cubeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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
}

View File

@ -96,7 +96,7 @@ func cubeRecipeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
}
for d.Next() {
record := &ExperienceBreakpointRecord{
record := &ExperienceBreakpointsRecord{
Level: d.Number("Level"),
HeroBreakpoints: map[d2enum.Hero]int{
d2enum.HeroAmazon: d.Number("Amazon"),
@ -68,7 +68,7 @@ func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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.Experience = breakpoints

View File

@ -4,14 +4,14 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// ExperienceBreakpoints describes the required experience
// for each level for each character class
type ExperienceBreakpoints map[int]*ExperienceBreakpointRecord
type ExperienceBreakpoints map[int]*ExperienceBreakpointsRecord
// ExperienceMaxLevels defines the max character levels
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
type ExperienceBreakpointRecord struct {
type ExperienceBreakpointsRecord struct {
Level int
HeroBreakpoints map[d2enum.Hero]int
Ratio int

View File

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

View File

@ -4,12 +4,12 @@ import (
"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 {
records := make(Gems)
for d.Next() {
gem := &GemRecord{
gem := &GemsRecord{
Name: d.String("name"),
Letter: d.String("letter"),
Transform: d.Number("transform"),
@ -60,7 +60,7 @@ func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Debugf("Loaded %d Gem records", len(records))
r.Logger.Infof("Loaded %d Gems records", len(records))
r.Item.Gems = records

View File

@ -1,11 +1,11 @@
package d2records
// Gems stores all of the GemRecords
type Gems map[string]*GemRecord
// Gems stores all of the GemsRecords
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
type GemRecord struct {
type GemsRecord struct {
Name string
Letter string
Transform int

View File

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

View File

@ -90,7 +90,7 @@ func hirelingLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Debugf("Loaded %d Hireling records", len(records))
r.Logger.Infof("Loaded %d Hireling records", len(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.Debugf("Loaded %d HitClass records", len(records))
r.Logger.Infof("Loaded %d HitClass records", len(records))
return nil
}

View File

@ -130,7 +130,7 @@ func inventoryLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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

View File

@ -70,7 +70,7 @@ func loadAffixDictionary(
}
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
}

View File

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

View File

@ -21,7 +21,7 @@ func lowQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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
}

View File

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

View File

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

View File

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

View File

@ -76,7 +76,7 @@ func itemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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.Equivalency = equivMap

View File

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

View File

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

View File

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

View File

@ -2,13 +2,13 @@ package d2records
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
// LevelDetails has all of the LevelDetailRecords
type LevelDetails map[int]*LevelDetailRecord
// LevelDetails has all of the LevelDetailsRecords
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,
// what kinds of monsters spawn, the level generator type, and lots of other stuff.
type LevelDetailRecord struct {
type LevelDetailsRecord struct {
// Name
// 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)
for d.Next() {
record := &LevelMazeDetailRecord{
record := &LevelMazeDetailsRecord{
Name: d.String("Name"),
LevelID: d.Number("Level"),
NumRoomsNormal: d.Number("Rooms"),
@ -24,7 +24,7 @@ func levelMazeDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return d.Err
}
r.Debugf("Loaded %d LevelMazeDetail records", len(records))
r.Logger.Infof("Loaded %d LevelMazeDetails records", len(records))
r.Level.Maze = records

View File

@ -1,11 +1,11 @@
package d2records
// LevelMazeDetails stores all of the LevelMazeDetailRecords
type LevelMazeDetails map[int]*LevelMazeDetailRecord
// LevelMazeDetails stores all of the LevelMazeDetailsRecords
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
type LevelMazeDetailRecord struct {
type LevelMazeDetailsRecord struct {
// descriptive, not loaded in game. Corresponds with Name field in
// Levels.txt
Name string // Name

View File

@ -42,7 +42,7 @@ func levelPresetLoader(r *RecordManager, d *d2txt.DataDictionary) error {
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 {
return d.Err

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
package d2records
// MonModes stores all of the MonModeRecords
// MonModes stores all of the GemsRecords
type MonModes map[string]*MonModeRecord
// 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