mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-02 22:57:04 -05:00
Backmerge master into ecs (#1021)
* quest log disabled icon
* removed unused functions for text colorizing
* terminal printing
* fixed saving key change bug
* help overlay text
* removed unused issue comments
* Update Ebiten to v2.0.2
* changed terminal color separator & changed logLevelNone to logLevelDefault in app.go
* status and screens update
* quest animation initial.
* escape menu hotkeys
* hero save file
* add-buttons init
* add-buttons actions
* stats changing: hero stats panel
* skill tre - remaining points label
* revert:hero save file (app.go)
* escape menu hotkeys
* hero save file
* updated d2hero.HeroStatsState
* corrected grammar errors
* animation is played and last frame is completedFrame
* animation stops playing, when quest log is closed & quest socket gets highlighted, when animation is playing & fixed highlight bug
* fixed quest descr bug & added code description
* level-up buttons tooltips
* Replaced kingping with flag package
* Cleanup d2records logging
* Renamed CharStatRecord
* Renamed SoundDetailRecord
* Renamed MonStatRecord
* Renamed ObjectDetailRecord
* Renamed MonStat2Record
* fixed onHover bug in d2ui.Sprite (#992)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* added static checks to d2ui (#990)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* fixed onHover bug in d2ui.Label (#991)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* Revert "fixed onHover bug in d2ui.Sprite"
This reverts commit 8b557062fb
.
* tip-labels in tcpip menu
* skill select menu dependencies (when we open skillselect menu, other panels are closed)
* Cleaned up d2term
* A wild } appeared
* Fixed linter issues
* ckecked value of italian modifier
* game-controls refactor
* fixed build and lint errors
* removed unnecessary switch-case statments from onKeyUp and onEscKey
* fixed bug with terminal's logLevel
* Refactor StreamWriter
* Refactor StreamReader
* Fixed gocritic linter issues
* Reduce GetTiles slice allocation
* Networking bugfixes and cleanup
Make sure connections close properly, without weird error messages
Remove player map entity when a player disconnects from a multiplayer game
Close server properly when host disconnects, handle ServerClose on remote clients
Don't mix JSON decoders and raw TCP writes
Actually handle incoming packets from remote clients
General code cleanup for simplicity and consistency
* Switched to self hosted build agent
* Removed PR requirement for action
* remove build job requirement
* Fiddling with actions names
* Updated workflows
* Fix yaml errors
* Yet another yaml fix
* Removed improper ebiten dependency in d2interface.
* Add checkboxes, checkbox test scene
* Render HUD before Panels (in this Case 'Panels' only does mean Inventory Panel). This is to avoid Entity Labels to be renderd above the Inventory Panel. Fixes #936
* This DCC frame size calculation seems useless
TBH there some to be some other overcomplicated things going on in
DCCAnimation but too tired to use brain right now.
* De-lint ecs branch
* bugfix: file_handle_resolver
* Add boot state to scenes, fix loading screen scene
* added label-button widget (#989)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
* removed links to closed issues from code (#1005)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
* removed unused fields from d2player.GameControl.actionableRegions (#997)
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
* d2ui.Frame refactor (#994)
* d2ui.Frame refactor
* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame
* d2ui.Frame refactor
* removed unneccessery d2asset.AssetManager argument from d2ui.NewUIFrame
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: Tim Sarbin <tim.sarbin@gmail.com>
Co-authored-by: gravestench <dknuth0101@gmail.com>
* d2mpq refactored (#1020)
* d2mpq refactor
* d2mpq refactor last standing lint error
* d2mpq refactor: less linter noise
* d2mpq refactor: more linter issues
Co-authored-by: M. Sz <mszeptuch@protonmail.com>
Co-authored-by: Tim Sarbin <tim.sarbin@gmail.com>
Co-authored-by: Hajime Hoshi <hajimehoshi@gmail.com>
Co-authored-by: gucio321 <73652197+gucio321@users.noreply.github.com>
Co-authored-by: Intyre <intyre@gmail.com>
Co-authored-by: ThomasChr <thomaschristlieb@hotmail.com>
Co-authored-by: Ziemas <ziemas@ziemas.se>
Co-authored-by: gravestench <dknuth0101@gmail.com>
This commit is contained in:
parent
0dee6518b4
commit
9121209f86
5
.github/workflows/pullRequest.yml
vendored
5
.github/workflows/pullRequest.yml
vendored
@ -1,10 +1,9 @@
|
||||
---
|
||||
name: pull_request
|
||||
"on": [pull_request]
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
name: ''
|
||||
runs-on: self-hosted
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Set up Go 1.14
|
||||
|
39
.github/workflows/pushToMaster.yml
vendored
39
.github/workflows/pushToMaster.yml
vendored
@ -1,39 +0,0 @@
|
||||
---
|
||||
name: build
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.14
|
||||
uses: actions/setup-go@v2.1.3
|
||||
with:
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xvfb libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libsdl2-dev libasound2-dev > /dev/null 2>&1
|
||||
|
||||
- name: Run golangci-lint
|
||||
continue-on-error: false
|
||||
uses: golangci/golangci-lint-action@v2.3.0
|
||||
with:
|
||||
version: v1.32
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
DISPLAY: ":99.0"
|
||||
run: |
|
||||
xvfb-run --auto-servernum go test -v -race ./...
|
||||
|
||||
- name: Build binary
|
||||
run: go build .
|
@ -25,7 +25,8 @@ ALL OTHER TRADEMARKS ARE THE PROPERTY OF THEIR RESPECTIVE OWNERS.
|
||||
|
||||
## Status
|
||||
|
||||
At the moment (october 2020) the game starts, you can select any character and run around Act1 town.
|
||||
At the moment (december 2020) the game starts, you can select any character and run around Act1 town.
|
||||
You can also open any of the game's panels.
|
||||
|
||||
Much work has been made in the background, but a lot of work still has to be done for the game to be playable.
|
||||
|
||||
@ -128,6 +129,8 @@ which will be updated over time with new requirements.
|
||||
|
||||
![Inventory Window](docs/Inventory.png)
|
||||
|
||||
![Game Panels](docs/game_panels.png)
|
||||
|
||||
## Additional Credits
|
||||
|
||||
- Diablo2 Logo
|
||||
|
296
d2app/app.go
296
d2app/app.go
@ -6,6 +6,7 @@ import (
|
||||
"container/ring"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
@ -24,7 +25,6 @@ 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,19 +85,12 @@ 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
|
||||
@ -110,21 +103,24 @@ const (
|
||||
|
||||
// Create creates a new instance of the application
|
||||
func Create(gitBranch, gitCommit string) *App {
|
||||
assetManager, assetError := d2asset.NewAssetManager()
|
||||
logger := d2util.NewLogger()
|
||||
logger.SetPrefix(appLoggerPrefix)
|
||||
|
||||
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.Logger = d2util.NewLogger()
|
||||
app.Logger.SetPrefix(appLoggerPrefix)
|
||||
app.Logger.SetLevel(d2util.LogLevelNone)
|
||||
app.parseArguments()
|
||||
|
||||
app.SetLevel(*app.Options.LogLevel)
|
||||
|
||||
app.asset, app.errorMessage = d2asset.NewAssetManager(*app.Options.LogLevel)
|
||||
|
||||
return app
|
||||
}
|
||||
@ -140,7 +136,7 @@ func (a *App) startDedicatedServer() error {
|
||||
srvChanIn := make(chan int)
|
||||
srvChanLog := make(chan string)
|
||||
|
||||
srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, a.config.LogLevel, maxPlayers)
|
||||
srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, *a.Options.LogLevel, maxPlayers)
|
||||
if srvErr != nil {
|
||||
return srvErr
|
||||
}
|
||||
@ -173,15 +169,7 @@ func (a *App) loadEngine() error {
|
||||
return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2")
|
||||
}
|
||||
|
||||
// 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)
|
||||
audio := ebiten2.CreateAudio(*a.Options.LogLevel, a.asset)
|
||||
|
||||
inputManager := d2input.NewInputManager()
|
||||
|
||||
@ -190,14 +178,9 @@ 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.config.LogLevel, audio)
|
||||
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio)
|
||||
|
||||
a.inputManager = inputManager
|
||||
a.terminal = term
|
||||
@ -206,50 +189,48 @@ 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 (
|
||||
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)"
|
||||
descProfile = "Profiles the program,\none of (cpu, mem, block, goroutine, trace, thread, mutex)"
|
||||
descPlayers = "Sets the number of max players for the dedicated server"
|
||||
descLogging = "Enables verbose logging. Log levels will include those below it.\n" +
|
||||
" 0 disables log messages\n" +
|
||||
" 1 shows fatal\n" +
|
||||
" 2 shows error\n" +
|
||||
" 3 shows warning\n" +
|
||||
" 4 shows info\n" +
|
||||
" 5 shows debug\n"
|
||||
)
|
||||
|
||||
a.Options.profiler = 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()
|
||||
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")
|
||||
|
||||
kingpin.Parse()
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("usage: %s [<flags>]\n\nFlags:\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if *a.Options.LogLevel >= d2util.LogLevelUnspecified {
|
||||
*a.Options.LogLevel = d2util.LogLevelDefault
|
||||
}
|
||||
|
||||
if *showVersion {
|
||||
a.Infof("version: OpenDiablo2 (%s %s)", a.gitBranch, a.gitCommit)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *showHelp {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfig loads the OpenDiablo2 config file
|
||||
@ -287,45 +268,15 @@ func (a *App) LoadConfig() (*d2config.Configuration, error) {
|
||||
}
|
||||
|
||||
// Run executes the application and kicks off the entire game process
|
||||
func (a *App) Run() error {
|
||||
a.parseArguments()
|
||||
|
||||
func (a *App) Run() (err error) {
|
||||
// add our possible config directories
|
||||
_, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()))
|
||||
_, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()))
|
||||
|
||||
config, err := a.LoadConfig()
|
||||
if err != nil {
|
||||
if a.config, err = a.LoadConfig(); 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)
|
||||
@ -389,36 +340,39 @@ func (a *App) initialize() error {
|
||||
a.renderer.SetWindowIcon("d2logo.png")
|
||||
a.terminal.BindLogger()
|
||||
|
||||
terminalActions := [...]bindTerminalEntry{
|
||||
{"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap},
|
||||
{"fullscreen", "toggles fullscreen", a.toggleFullScreen},
|
||||
{"capframe", "captures a still frame", a.setupCaptureFrame},
|
||||
{"capgifstart", "captures an animation (start)", a.startAnimationCapture},
|
||||
{"capgifstop", "captures an animation (stop)", a.stopAnimationCapture},
|
||||
{"vsync", "toggles vsync", a.toggleVsync},
|
||||
{"fps", "toggle fps counter", a.toggleFpsCounter},
|
||||
{"timescale", "set scalar for elapsed time", a.setTimeScale},
|
||||
{"quit", "exits the game", a.quitGame},
|
||||
{"screen-gui", "enters the gui playground screen", a.enterGuiPlayground},
|
||||
{"js", "eval JS scripts", a.evalJS},
|
||||
terminalCommands := []struct {
|
||||
name string
|
||||
desc string
|
||||
args []string
|
||||
fn func(args []string) error
|
||||
}{
|
||||
{"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap},
|
||||
{"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen},
|
||||
{"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame},
|
||||
{"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture},
|
||||
{"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture},
|
||||
{"vsync", "toggles vsync", nil, a.toggleVsync},
|
||||
{"fps", "toggle fps counter", nil, a.toggleFpsCounter},
|
||||
{"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale},
|
||||
{"quit", "exits the game", nil, a.quitGame},
|
||||
{"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground},
|
||||
{"js", "eval JS scripts", []string{"code"}, a.evalJS},
|
||||
}
|
||||
|
||||
for idx := range terminalActions {
|
||||
action := &terminalActions[idx]
|
||||
|
||||
if err := a.terminal.BindAction(action.name, action.description, action.action); err != nil {
|
||||
a.Fatal(err.Error())
|
||||
for _, cmd := range terminalCommands {
|
||||
if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil {
|
||||
a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
gui, err := d2gui.CreateGuiManager(a.asset, a.config.LogLevel, a.inputManager)
|
||||
gui, err := d2gui.CreateGuiManager(a.asset, *a.Options.LogLevel, a.inputManager)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.guiManager = gui
|
||||
|
||||
a.screen = d2screen.NewScreenManager(a.ui, a.config.LogLevel, a.guiManager)
|
||||
a.screen = d2screen.NewScreenManager(a.ui, *a.Options.LogLevel, a.guiManager)
|
||||
|
||||
a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)
|
||||
|
||||
@ -682,7 +636,7 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
|
||||
return deltaAllocPerFrame * fps / bytesToMegabyte
|
||||
}
|
||||
|
||||
func (a *App) dumpHeap() {
|
||||
func (a *App) dumpHeap([]string) error {
|
||||
if _, err := os.Stat("./pprof/"); os.IsNotExist(err) {
|
||||
if err := os.Mkdir("./pprof/", 0750); err != nil {
|
||||
a.Fatal(err.Error())
|
||||
@ -701,48 +655,56 @@ func (a *App) dumpHeap() {
|
||||
if err := fileOut.Close(); err != nil {
|
||||
a.Fatal(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) evalJS(code string) {
|
||||
val, err := a.scriptEngine.Eval(code)
|
||||
func (a *App) evalJS(args []string) error {
|
||||
val, err := a.scriptEngine.Eval(args[0])
|
||||
if err != nil {
|
||||
a.terminal.OutputErrorf("%s", err)
|
||||
return
|
||||
a.terminal.Errorf(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
a.Info("%s" + val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) toggleFullScreen() {
|
||||
func (a *App) toggleFullScreen([]string) error {
|
||||
fullscreen := !a.renderer.IsFullScreen()
|
||||
a.renderer.SetFullScreen(fullscreen)
|
||||
a.terminal.OutputInfof("fullscreen is now: %v", fullscreen)
|
||||
a.terminal.Infof("fullscreen is now: %v", fullscreen)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) setupCaptureFrame(path string) {
|
||||
func (a *App) setupCaptureFrame(args []string) error {
|
||||
a.captureState = captureStateFrame
|
||||
a.capturePath = path
|
||||
a.capturePath = args[0]
|
||||
a.captureFrames = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) doCaptureFrame(target d2interface.Surface) error {
|
||||
fp, err := os.Create(a.capturePath)
|
||||
if err != nil {
|
||||
a.terminal.Errorf("failed to create %q", a.capturePath)
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := fp.Close(); err != nil {
|
||||
a.Fatal(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
screenshot := target.Screenshot()
|
||||
if err := png.Encode(fp, screenshot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Info(fmt.Sprintf("saved frame to %s", a.capturePath))
|
||||
if err := fp.Close(); err != nil {
|
||||
a.terminal.Errorf("failed to create %q", a.capturePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
a.terminal.Infof("saved frame to %s", a.capturePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -802,47 +764,61 @@ func (a *App) convertFramesToGif() error {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Info(fmt.Sprintf("saved animation to %s", a.capturePath))
|
||||
a.Infof("saved animation to %s", a.capturePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) startAnimationCapture(path string) {
|
||||
func (a *App) startAnimationCapture(args []string) error {
|
||||
a.captureState = captureStateGif
|
||||
a.capturePath = path
|
||||
a.capturePath = args[0]
|
||||
a.captureFrames = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) stopAnimationCapture() {
|
||||
func (a *App) stopAnimationCapture([]string) error {
|
||||
a.captureState = captureStateNone
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) toggleVsync() {
|
||||
func (a *App) toggleVsync([]string) error {
|
||||
vsync := !a.renderer.GetVSyncEnabled()
|
||||
a.renderer.SetVSyncEnabled(vsync)
|
||||
a.terminal.OutputInfof("vsync is now: %v", vsync)
|
||||
a.terminal.Infof("vsync is now: %v", vsync)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) toggleFpsCounter() {
|
||||
func (a *App) toggleFpsCounter([]string) error {
|
||||
a.showFPS = !a.showFPS
|
||||
a.terminal.OutputInfof("fps counter is now: %v", a.showFPS)
|
||||
a.terminal.Infof("fps counter is now: %v", a.showFPS)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) setTimeScale(timeScale float64) {
|
||||
if timeScale <= 0 {
|
||||
a.terminal.OutputErrorf("invalid time scale value")
|
||||
} else {
|
||||
a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale)
|
||||
func (a *App) setTimeScale(args []string) error {
|
||||
timeScale, err := strconv.ParseFloat(args[0], 64)
|
||||
if err != nil || timeScale <= 0 {
|
||||
a.terminal.Errorf("invalid time scale value")
|
||||
return nil
|
||||
}
|
||||
|
||||
a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale)
|
||||
a.timeScale = timeScale
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) quitGame() {
|
||||
func (a *App) quitGame([]string) error {
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) enterGuiPlayground() {
|
||||
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, a.config.LogLevel, a.asset))
|
||||
func (a *App) enterGuiPlayground([]string) error {
|
||||
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset))
|
||||
return nil
|
||||
}
|
||||
|
||||
func createZeroedRing(n int) *ring.Ring {
|
||||
@ -911,7 +887,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.config.LogLevel, errorMessageOptional...)
|
||||
*a.Options.LogLevel, errorMessageOptional...)
|
||||
if err != nil {
|
||||
a.Error(err.Error())
|
||||
return
|
||||
@ -922,7 +898,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.config.LogLevel, host)
|
||||
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, *a.Options.LogLevel, host)
|
||||
if err != nil {
|
||||
a.Error(err.Error())
|
||||
return
|
||||
@ -933,18 +909,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.config.LogLevel, a.scriptEngine)
|
||||
gameClient, err := d2client.Create(connType, a.asset, *a.Options.LogLevel, a.scriptEngine)
|
||||
if err != nil {
|
||||
a.Error(err.Error())
|
||||
}
|
||||
|
||||
if err = gameClient.Open(host, filePath); err != nil {
|
||||
errorMessage := fmt.Sprintf("can not connect to the host: %s", host)
|
||||
fmt.Println(errorMessage)
|
||||
a.Error(errorMessage)
|
||||
a.ToMainMenu(errorMessage)
|
||||
} else {
|
||||
game, err := d2gamescreen.CreateGame(
|
||||
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, a.config.LogLevel, a.guiManager,
|
||||
a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, *a.Options.LogLevel, a.guiManager,
|
||||
)
|
||||
if err != nil {
|
||||
a.Error(err.Error())
|
||||
@ -957,9 +933,9 @@ func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.Clie
|
||||
// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
|
||||
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.config.LogLevel, connHost)
|
||||
a.audio, a.ui, connType, *a.Options.LogLevel, connHost)
|
||||
if err != nil {
|
||||
fmt.Printf("unable to create character select screen: %s", err)
|
||||
a.Errorf("unable to create character select screen: %s", err)
|
||||
}
|
||||
|
||||
a.screen.SetNextScreen(characterSelect)
|
||||
@ -968,7 +944,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.config.LogLevel, a.screen)
|
||||
*a.Options.LogLevel, a.screen)
|
||||
if err != nil {
|
||||
a.Error(err.Error())
|
||||
return
|
||||
@ -979,10 +955,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.config.LogLevel, a.ui))
|
||||
a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, *a.Options.LogLevel, a.ui))
|
||||
}
|
||||
|
||||
// ToCinematics forces the game to transition to the cinematics menu
|
||||
func (a *App) ToCinematics() {
|
||||
a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, a.config.LogLevel, a.ui))
|
||||
a.screen.SetNextScreen(d2gamescreen.CreateCinematics(a, a.asset, a.renderer, a.audio, *a.Options.LogLevel, a.ui))
|
||||
}
|
||||
|
@ -4,12 +4,6 @@ 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
|
||||
@ -26,16 +20,6 @@ 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]
|
||||
@ -44,32 +28,46 @@ 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 {
|
||||
var result int16
|
||||
|
||||
for offset := uint64(0); offset < bytesPerInt16; offset++ {
|
||||
shift := uint8(bitsPerByte * offset)
|
||||
result += int16(v.data[v.position+offset]) << shift
|
||||
return int16(v.GetUInt16())
|
||||
}
|
||||
|
||||
v.position += bytesPerInt16
|
||||
// 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
|
||||
}
|
||||
|
||||
return result
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetPosition sets the stream position with the given position
|
||||
@ -77,51 +75,9 @@ func (v *StreamReader) SetPosition(newPosition uint64) {
|
||||
v.position = newPosition
|
||||
}
|
||||
|
||||
// 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())
|
||||
// GetSize returns the total size of the stream in bytes
|
||||
func (v *StreamReader) GetSize() uint64 {
|
||||
return uint64(len(v.data))
|
||||
}
|
||||
|
||||
// ReadByte implements io.ByteReader
|
||||
|
@ -2,10 +2,6 @@ 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
|
||||
@ -20,41 +16,40 @@ 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) {
|
||||
for count := 0; count < bytesPerInt16; count++ {
|
||||
shift := count * bitsPerByte
|
||||
v.data.WriteByte(byte(val>>shift) & byteMask)
|
||||
v.PushUint16(uint16(val))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
v.data.WriteByte(byte(val))
|
||||
v.data.WriteByte(byte(val >> 8))
|
||||
v.data.WriteByte(byte(val >> 16))
|
||||
v.data.WriteByte(byte(val >> 24))
|
||||
}
|
||||
|
||||
// PushInt64 writes a uint64 qword to the stream
|
||||
@ -62,7 +57,15 @@ func (v *StreamWriter) PushInt64(val int64) {
|
||||
v.PushUint64(uint64(val))
|
||||
}
|
||||
|
||||
// GetBytes returns the the byte slice of the underlying data
|
||||
func (v *StreamWriter) GetBytes() []byte {
|
||||
return v.data.Bytes()
|
||||
// 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))
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package d2enum
|
||||
|
||||
// there are labels for "numeric labels (see AssetManager.TranslateLabel)
|
||||
const (
|
||||
CancelLabel = iota
|
||||
RepairAll = iota
|
||||
_
|
||||
CancelLabel
|
||||
CopyrightLabel
|
||||
AllRightsReservedLabel
|
||||
SinglePlayerLabel
|
||||
@ -62,6 +64,8 @@ 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
|
||||
|
11
d2common/d2enum/scene_state.go
Normal file
11
d2common/d2enum/scene_state.go
Normal file
@ -0,0 +1,11 @@
|
||||
package d2enum
|
||||
|
||||
// SceneState enumerates the different states a scene can be in
|
||||
type SceneState int
|
||||
|
||||
// Scene states
|
||||
const (
|
||||
SceneStateUninitialized SceneState = iota
|
||||
SceneStateBooting
|
||||
SceneStateBooted
|
||||
)
|
131
d2common/d2fileformats/d2mpq/crypto.go
Normal file
131
d2common/d2fileformats/d2mpq/crypto.go
Normal file
@ -0,0 +1,131 @@
|
||||
package d2mpq
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
|
||||
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
|
||||
|
||||
func cryptoLookup(index uint32) uint32 {
|
||||
if !cryptoBufferReady {
|
||||
cryptoInitialize()
|
||||
|
||||
cryptoBufferReady = true
|
||||
}
|
||||
|
||||
return cryptoBuffer[index]
|
||||
}
|
||||
|
||||
//nolint:gomnd // Decryption magic
|
||||
func cryptoInitialize() {
|
||||
seed := uint32(0x00100001)
|
||||
|
||||
for index1 := 0; index1 < 0x100; index1++ {
|
||||
index2 := index1
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
seed = (seed*125 + 3) % 0x2AAAAB
|
||||
temp1 := (seed & 0xFFFF) << 0x10
|
||||
seed = (seed*125 + 3) % 0x2AAAAB
|
||||
temp2 := seed & 0xFFFF
|
||||
cryptoBuffer[index2] = temp1 | temp2
|
||||
index2 += 0x100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gomnd // Decryption magic
|
||||
func decrypt(data []uint32, seed uint32) {
|
||||
seed2 := uint32(0xeeeeeeee)
|
||||
|
||||
for i := 0; i < len(data); i++ {
|
||||
seed2 += cryptoLookup(0x400 + (seed & 0xff))
|
||||
result := data[i]
|
||||
result ^= seed + seed2
|
||||
|
||||
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||
seed2 = result + seed2 + (seed2 << 5) + 3
|
||||
data[i] = result
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gomnd // Decryption magic
|
||||
func decryptBytes(data []byte, seed uint32) {
|
||||
seed2 := uint32(0xEEEEEEEE)
|
||||
for i := 0; i < len(data)-3; i += 4 {
|
||||
seed2 += cryptoLookup(0x400 + (seed & 0xFF))
|
||||
result := binary.LittleEndian.Uint32(data[i : i+4])
|
||||
result ^= seed + seed2
|
||||
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||
seed2 = result + seed2 + (seed2 << 5) + 3
|
||||
|
||||
data[i+0] = uint8(result & 0xff)
|
||||
data[i+1] = uint8((result >> 8) & 0xff)
|
||||
data[i+2] = uint8((result >> 16) & 0xff)
|
||||
data[i+3] = uint8((result >> 24) & 0xff)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gomnd // Decryption magic
|
||||
func decryptTable(r io.Reader, size uint32, name string) ([]uint32, error) {
|
||||
seed := hashString(name, 3)
|
||||
seed2 := uint32(0xEEEEEEEE)
|
||||
size *= 4
|
||||
|
||||
table := make([]uint32, size)
|
||||
buf := make([]byte, 4)
|
||||
|
||||
for i := uint32(0); i < size; i++ {
|
||||
seed2 += cryptoBuffer[0x400+(seed&0xff)]
|
||||
|
||||
if _, err := r.Read(buf); err != nil {
|
||||
return table, err
|
||||
}
|
||||
|
||||
result := binary.LittleEndian.Uint32(buf)
|
||||
result ^= seed + seed2
|
||||
|
||||
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||
seed2 = result + seed2 + (seed2 << 5) + 3
|
||||
table[i] = result
|
||||
}
|
||||
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func hashFilename(key string) uint64 {
|
||||
a, b := hashString(key, 1), hashString(key, 2)
|
||||
return uint64(a)<<32 | uint64(b)
|
||||
}
|
||||
|
||||
//nolint:gomnd // Decryption magic
|
||||
func hashString(key string, hashType uint32) uint32 {
|
||||
seed1 := uint32(0x7FED7FED)
|
||||
seed2 := uint32(0xEEEEEEEE)
|
||||
|
||||
/* prepare seeds. */
|
||||
for _, char := range strings.ToUpper(key) {
|
||||
seed1 = cryptoLookup((hashType*0x100)+uint32(char)) ^ (seed1 + seed2)
|
||||
seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3
|
||||
}
|
||||
|
||||
return seed1
|
||||
}
|
||||
|
||||
//nolint:unused,deadcode,gomnd // will use this for creating mpq's
|
||||
func encrypt(data []uint32, seed uint32) {
|
||||
seed2 := uint32(0xeeeeeeee)
|
||||
|
||||
for i := 0; i < len(data); i++ {
|
||||
seed2 += cryptoLookup(0x400 + (seed & 0xff))
|
||||
result := data[i]
|
||||
result ^= seed + seed2
|
||||
|
||||
seed = ((^seed << 21) + 0x11111111) | (seed >> 11)
|
||||
seed2 = data[i] + seed2 + (seed2 << 5) + 3
|
||||
data[i] = result
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package d2mpq
|
||||
|
||||
var cryptoBuffer [0x500]uint32 //nolint:gochecknoglobals // will fix later..
|
||||
var cryptoBufferReady bool //nolint:gochecknoglobals // will fix later..
|
||||
|
||||
func cryptoLookup(index uint32) uint32 {
|
||||
if !cryptoBufferReady {
|
||||
cryptoInitialize()
|
||||
|
||||
cryptoBufferReady = true
|
||||
}
|
||||
|
||||
return cryptoBuffer[index]
|
||||
}
|
||||
|
||||
//nolint:gomnd // magic cryptographic stuff here...
|
||||
func cryptoInitialize() {
|
||||
seed := uint32(0x00100001)
|
||||
|
||||
for index1 := 0; index1 < 0x100; index1++ {
|
||||
index2 := index1
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
seed = (seed*125 + 3) % 0x2AAAAB
|
||||
temp1 := (seed & 0xFFFF) << 0x10
|
||||
seed = (seed*125 + 3) % 0x2AAAAB
|
||||
temp2 := seed & 0xFFFF
|
||||
cryptoBuffer[index2] = temp1 | temp2
|
||||
index2 += 0x100
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package d2mpq
|
||||
|
||||
// HashEntryMap represents a hash entry map
|
||||
type HashEntryMap struct {
|
||||
entries map[uint64]HashTableEntry
|
||||
}
|
||||
|
||||
// Insert inserts a hash entry into the table
|
||||
func (hem *HashEntryMap) Insert(entry *HashTableEntry) {
|
||||
if hem.entries == nil {
|
||||
hem.entries = make(map[uint64]HashTableEntry)
|
||||
}
|
||||
|
||||
hem.entries[uint64(entry.NamePartA)<<32|uint64(entry.NamePartB)] = *entry
|
||||
}
|
||||
|
||||
// Find finds a hash entry
|
||||
func (hem *HashEntryMap) Find(fileName string) (*HashTableEntry, bool) {
|
||||
if hem.entries == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
hashA := hashString(fileName, 1)
|
||||
hashB := hashString(fileName, 2)
|
||||
|
||||
entry, found := hem.entries[uint64(hashA)<<32|uint64(hashB)]
|
||||
|
||||
return &entry, found
|
||||
}
|
||||
|
||||
// Contains returns true if the hash entry contains the values
|
||||
func (hem *HashEntryMap) Contains(fileName string) bool {
|
||||
_, found := hem.Find(fileName)
|
||||
return found
|
||||
}
|
@ -2,10 +2,9 @@ package d2mpq
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@ -21,31 +20,9 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
|
||||
type MPQ struct {
|
||||
filePath string
|
||||
file *os.File
|
||||
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
|
||||
hashes map[uint64]*Hash
|
||||
blocks []*Block
|
||||
header Header
|
||||
}
|
||||
|
||||
// PatchInfo represents patch info for the MPQ.
|
||||
@ -53,71 +30,153 @@ 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
|
||||
}
|
||||
|
||||
// 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}
|
||||
// New loads an MPQ file and only reads the header
|
||||
func New(fileName string) (*MPQ, error) {
|
||||
mpq := &MPQ{filePath: fileName}
|
||||
|
||||
var err error
|
||||
if runtime.GOOS == "linux" {
|
||||
result.file, err = openIgnoreCase(fileName)
|
||||
mpq.file, err = openIgnoreCase(fileName)
|
||||
} else {
|
||||
result.file, err = os.Open(fileName) //nolint:gosec // Will fix later
|
||||
mpq.file, err = os.Open(fileName) //nolint:gosec // Will fix later
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := result.readHeader(); err != nil {
|
||||
if err := mpq.readHeader(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read reader: %v", err)
|
||||
}
|
||||
|
||||
return mpq, nil
|
||||
}
|
||||
|
||||
// FromFile loads an MPQ file and returns a MPQ structure
|
||||
func FromFile(fileName string) (*MPQ, error) {
|
||||
mpq, err := New(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
if err := mpq.readHashTable(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read hash table: %v", err)
|
||||
}
|
||||
|
||||
if err := mpq.readBlockTable(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read block table: %v", err)
|
||||
}
|
||||
|
||||
return mpq, nil
|
||||
}
|
||||
|
||||
// getFileBlockData gets a block table entry
|
||||
func (mpq *MPQ) getFileBlockData(fileName string) (*Block, error) {
|
||||
fileEntry, ok := mpq.hashes[hashFilename(fileName)]
|
||||
if !ok {
|
||||
return nil, errors.New("file not found")
|
||||
}
|
||||
|
||||
if fileEntry.BlockIndex >= uint32(len(mpq.blocks)) {
|
||||
return nil, errors.New("invalid block index")
|
||||
}
|
||||
|
||||
return mpq.blocks[fileEntry.BlockIndex], nil
|
||||
}
|
||||
|
||||
// Close closes the MPQ file
|
||||
func (mpq *MPQ) Close() error {
|
||||
return mpq.file.Close()
|
||||
}
|
||||
|
||||
// ReadFile reads a file from the MPQ and returns a memory stream
|
||||
func (mpq *MPQ) ReadFile(fileName string) ([]byte, error) {
|
||||
fileBlockData, err := mpq.getFileBlockData(fileName)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
fileBlockData.FileName = strings.ToLower(fileName)
|
||||
|
||||
stream, err := CreateStream(mpq, fileBlockData, fileName)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
buffer := make([]byte, fileBlockData.UncompressedFileSize)
|
||||
if _, err := stream.Read(buffer, 0, fileBlockData.UncompressedFileSize); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
// ReadFileStream reads the mpq file data and returns a stream
|
||||
func (mpq *MPQ) ReadFileStream(fileName string) (d2interface.DataStream, error) {
|
||||
fileBlockData, err := mpq.getFileBlockData(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileBlockData.FileName = strings.ToLower(fileName)
|
||||
|
||||
stream, err := CreateStream(mpq, fileBlockData, fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MpqDataStream{stream: stream}, nil
|
||||
}
|
||||
|
||||
// ReadTextFile reads a file and returns it as a string
|
||||
func (mpq *MPQ) ReadTextFile(fileName string) (string, error) {
|
||||
data, err := mpq.ReadFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Listfile returns the list of files in this MPQ
|
||||
func (mpq *MPQ) Listfile() ([]string, error) {
|
||||
data, err := mpq.ReadFile("(listfile)")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw := strings.TrimRight(string(data), "\x00")
|
||||
s := bufio.NewScanner(strings.NewReader(raw))
|
||||
|
||||
var filePaths []string
|
||||
|
||||
for s.Scan() {
|
||||
filePath := s.Text()
|
||||
filePaths = append(filePaths, filePath)
|
||||
}
|
||||
|
||||
return filePaths, nil
|
||||
}
|
||||
|
||||
// Path returns the MPQ file path
|
||||
func (mpq *MPQ) Path() string {
|
||||
return mpq.filePath
|
||||
}
|
||||
|
||||
// Contains returns bool for whether the given filename exists in the mpq
|
||||
func (mpq *MPQ) Contains(filename string) bool {
|
||||
_, ok := mpq.hashes[hashFilename(filename)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Size returns the size of the mpq in bytes
|
||||
func (mpq *MPQ) Size() uint32 {
|
||||
return mpq.header.ArchiveSize
|
||||
}
|
||||
|
||||
func openIgnoreCase(mpqPath string) (*os.File, error) {
|
||||
@ -142,258 +201,5 @@ func openIgnoreCase(mpqPath string) (*os.File, error) {
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later
|
||||
|
||||
return 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
|
||||
return os.Open(path.Join(mpqDir, mpqName)) //nolint:gosec // Will fix later
|
||||
}
|
||||
|
77
d2common/d2fileformats/d2mpq/mpq_block.go
Normal file
77
d2common/d2fileformats/d2mpq/mpq_block.go
Normal file
@ -0,0 +1,77 @@
|
||||
package d2mpq
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileFlag represents flags for a file record in the MPQ archive
|
||||
type FileFlag uint32
|
||||
|
||||
const (
|
||||
// FileImplode - File is compressed using PKWARE Data compression library
|
||||
FileImplode FileFlag = 0x00000100
|
||||
// FileCompress - File is compressed using combination of compression methods
|
||||
FileCompress FileFlag = 0x00000200
|
||||
// FileEncrypted - The file is encrypted
|
||||
FileEncrypted FileFlag = 0x00010000
|
||||
// FileFixKey - The decryption key for the file is altered according to the position of the file in the archive
|
||||
FileFixKey FileFlag = 0x00020000
|
||||
// FilePatchFile - The file contains incremental patch for an existing file in base MPQ
|
||||
FilePatchFile FileFlag = 0x00100000
|
||||
// FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit
|
||||
FileSingleUnit FileFlag = 0x01000000
|
||||
// FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch
|
||||
// archives to delete files present in lower-priority archives in the search chain. The file usually
|
||||
// has length of 0 or 1 byte and its name is a hash
|
||||
FileDeleteMarker FileFlag = 0x02000000
|
||||
// FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded.
|
||||
FileSectorCrc FileFlag = 0x04000000
|
||||
// FileExists - Set if file exists, reset when the file was deleted
|
||||
FileExists FileFlag = 0x80000000
|
||||
)
|
||||
|
||||
// Block represents an entry in the block table
|
||||
type Block struct { // 16 bytes
|
||||
FilePosition uint32
|
||||
CompressedFileSize uint32
|
||||
UncompressedFileSize uint32
|
||||
Flags FileFlag
|
||||
// Local Stuff...
|
||||
FileName string
|
||||
EncryptionSeed uint32
|
||||
}
|
||||
|
||||
// HasFlag returns true if the specified flag is present
|
||||
func (b *Block) HasFlag(flag FileFlag) bool {
|
||||
return (b.Flags & flag) != 0
|
||||
}
|
||||
|
||||
func (b *Block) calculateEncryptionSeed(fileName string) {
|
||||
fileName = fileName[strings.LastIndex(fileName, `\`)+1:]
|
||||
seed := hashString(fileName, 3)
|
||||
b.EncryptionSeed = (seed + b.FilePosition) ^ b.UncompressedFileSize
|
||||
}
|
||||
|
||||
//nolint:gomnd // number
|
||||
func (mpq *MPQ) readBlockTable() error {
|
||||
if _, err := mpq.file.Seek(int64(mpq.header.BlockTableOffset), io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blockData, err := decryptTable(mpq.file, mpq.header.BlockTableEntries, "(block table)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for n, i := uint32(0), uint32(0); i < mpq.header.BlockTableEntries; n, i = n+4, i+1 {
|
||||
mpq.blocks = append(mpq.blocks, &Block{
|
||||
FilePosition: blockData[n],
|
||||
CompressedFileSize: blockData[n+1],
|
||||
UncompressedFileSize: blockData[n+2],
|
||||
Flags: FileFlag(blockData[n+3]),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -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 := m.stream.Read(p, 0, uint32(len(p)))
|
||||
return int(totalRead), nil
|
||||
totalRead, err := m.stream.Read(p, 0, uint32(len(p)))
|
||||
return int(totalRead), err
|
||||
}
|
||||
|
||||
// Seek sets the position of the data stream
|
||||
func (m *MpqDataStream) Seek(offset int64, whence int) (int64, error) {
|
||||
m.stream.CurrentPosition = uint32(offset + int64(whence))
|
||||
return int64(m.stream.CurrentPosition), nil
|
||||
m.stream.Position = uint32(offset + int64(whence))
|
||||
return int64(m.stream.Position), nil
|
||||
}
|
||||
|
||||
// Close closes the data stream
|
||||
|
45
d2common/d2fileformats/d2mpq/mpq_hash.go
Normal file
45
d2common/d2fileformats/d2mpq/mpq_hash.go
Normal file
@ -0,0 +1,45 @@
|
||||
package d2mpq
|
||||
|
||||
import "io"
|
||||
|
||||
// Hash represents a hashed file entry in the MPQ file
|
||||
type Hash struct { // 16 bytes
|
||||
A uint32
|
||||
B uint32
|
||||
Locale uint16
|
||||
Platform uint16
|
||||
BlockIndex uint32
|
||||
}
|
||||
|
||||
// Name64 returns part A and B as uint64
|
||||
func (h *Hash) Name64() uint64 {
|
||||
return uint64(h.A)<<32 | uint64(h.B)
|
||||
}
|
||||
|
||||
//nolint:gomnd // number
|
||||
func (mpq *MPQ) readHashTable() error {
|
||||
if _, err := mpq.file.Seek(int64(mpq.header.HashTableOffset), io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashData, err := decryptTable(mpq.file, mpq.header.HashTableEntries, "(hash table)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mpq.hashes = make(map[uint64]*Hash)
|
||||
|
||||
for n, i := uint32(0), uint32(0); i < mpq.header.HashTableEntries; n, i = n+4, i+1 {
|
||||
e := &Hash{
|
||||
A: hashData[n],
|
||||
B: hashData[n+1],
|
||||
// https://github.com/OpenDiablo2/OpenDiablo2/issues/812
|
||||
Locale: uint16(hashData[n+2] >> 16), //nolint:gomnd // // binary data
|
||||
Platform: uint16(hashData[n+2] & 0xFFFF), //nolint:gomnd // // binary data
|
||||
BlockIndex: hashData[n+3],
|
||||
}
|
||||
mpq.hashes[e.Name64()] = e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
36
d2common/d2fileformats/d2mpq/mpq_header.go
Normal file
36
d2common/d2fileformats/d2mpq/mpq_header.go
Normal file
@ -0,0 +1,36 @@
|
||||
package d2mpq
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Header Represents a MPQ file
|
||||
type Header struct {
|
||||
Magic [4]byte
|
||||
HeaderSize uint32
|
||||
ArchiveSize uint32
|
||||
FormatVersion uint16
|
||||
BlockSize uint16
|
||||
HashTableOffset uint32
|
||||
BlockTableOffset uint32
|
||||
HashTableEntries uint32
|
||||
BlockTableEntries uint32
|
||||
}
|
||||
|
||||
func (mpq *MPQ) readHeader() error {
|
||||
if _, err := mpq.file.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Read(mpq.file, binary.LittleEndian, &mpq.header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(mpq.header.Magic[:]) != "MPQ\x1A" {
|
||||
return errors.New("invalid mpq header")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -6,8 +6,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"io"
|
||||
|
||||
"github.com/JoshVarga/blast"
|
||||
|
||||
@ -17,80 +16,63 @@ import (
|
||||
|
||||
// Stream represents a stream of data in an MPQ archive
|
||||
type Stream struct {
|
||||
BlockTableEntry BlockTableEntry
|
||||
BlockPositions []uint32
|
||||
CurrentData []byte
|
||||
FileName string
|
||||
MPQData *MPQ
|
||||
EncryptionSeed uint32
|
||||
CurrentPosition uint32
|
||||
CurrentBlockIndex uint32
|
||||
BlockSize uint32
|
||||
Data []byte
|
||||
Positions []uint32
|
||||
MPQ *MPQ
|
||||
Block *Block
|
||||
Index uint32
|
||||
Size uint32
|
||||
Position uint32
|
||||
}
|
||||
|
||||
// CreateStream creates an MPQ stream
|
||||
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
|
||||
func CreateStream(mpq *MPQ, block *Block, fileName string) (*Stream, error) {
|
||||
s := &Stream{
|
||||
MPQ: mpq,
|
||||
Block: block,
|
||||
Index: 0xFFFFFFFF, //nolint:gomnd // MPQ magic
|
||||
}
|
||||
|
||||
result.BlockSize = 0x200 << result.MPQData.data.BlockSize //nolint:gomnd // MPQ magic
|
||||
|
||||
if result.BlockTableEntry.HasFlag(FilePatchFile) {
|
||||
log.Fatal("Patching is not supported")
|
||||
if s.Block.HasFlag(FileFixKey) {
|
||||
s.Block.calculateEncryptionSeed(fileName)
|
||||
}
|
||||
|
||||
var err error
|
||||
s.Size = 0x200 << s.MPQ.header.BlockSize //nolint:gomnd // MPQ magic
|
||||
|
||||
if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) &&
|
||||
!result.BlockTableEntry.HasFlag(FileSingleUnit) {
|
||||
err = result.loadBlockOffsets()
|
||||
if s.Block.HasFlag(FilePatchFile) {
|
||||
return nil, errors.New("patching is not supported")
|
||||
}
|
||||
|
||||
return result, err
|
||||
if (s.Block.HasFlag(FileCompress) || s.Block.HasFlag(FileImplode)) && !s.Block.HasFlag(FileSingleUnit) {
|
||||
if err := s.loadBlockOffsets(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (v *Stream) loadBlockOffsets() error {
|
||||
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 {
|
||||
if _, err := v.MPQ.file.Seek(int64(v.Block.FilePosition), io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mpqBytes := make([]byte, blockPositionCount*4) //nolint:gomnd // MPQ magic
|
||||
blockPositionCount := ((v.Block.UncompressedFileSize + v.Size - 1) / v.Size) + 1
|
||||
v.Positions = make([]uint32, blockPositionCount)
|
||||
|
||||
_, err = v.MPQData.file.Read(mpqBytes)
|
||||
if err != nil {
|
||||
if err := binary.Read(v.MPQ.file, binary.LittleEndian, &v.Positions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range v.BlockPositions {
|
||||
idx := i * 4 //nolint:gomnd // MPQ magic
|
||||
v.BlockPositions[i] = binary.LittleEndian.Uint32(mpqBytes[idx : idx+4])
|
||||
}
|
||||
if v.Block.HasFlag(FileEncrypted) {
|
||||
decrypt(v.Positions, v.Block.EncryptionSeed-1)
|
||||
|
||||
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
|
||||
|
||||
if v.BlockTableEntry.HasFlag(FileEncrypted) {
|
||||
decrypt(v.BlockPositions, v.EncryptionSeed-1)
|
||||
|
||||
if v.BlockPositions[0] != blockPosSize {
|
||||
log.Println("Decryption of MPQ failed!")
|
||||
if v.Positions[0] != blockPosSize {
|
||||
return errors.New("decryption of MPQ failed")
|
||||
}
|
||||
|
||||
if v.BlockPositions[1] > v.BlockSize+blockPosSize {
|
||||
log.Println("Decryption of MPQ failed!")
|
||||
if v.Positions[1] > v.Size+blockPosSize {
|
||||
return errors.New("decryption of MPQ failed")
|
||||
}
|
||||
}
|
||||
@ -98,16 +80,18 @@ func (v *Stream) loadBlockOffsets() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
|
||||
if v.BlockTableEntry.HasFlag(FileSingleUnit) {
|
||||
func (v *Stream) Read(buffer []byte, offset, count uint32) (readTotal uint32, err error) {
|
||||
if v.Block.HasFlag(FileSingleUnit) {
|
||||
return v.readInternalSingleUnit(buffer, offset, count)
|
||||
}
|
||||
|
||||
toRead := count
|
||||
readTotal := uint32(0)
|
||||
var read uint32
|
||||
|
||||
toRead := count
|
||||
for toRead > 0 {
|
||||
read := v.readInternal(buffer, offset, toRead)
|
||||
if read, err = v.readInternal(buffer, offset, toRead); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if read == 0 {
|
||||
break
|
||||
@ -118,149 +102,153 @@ func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 {
|
||||
toRead -= read
|
||||
}
|
||||
|
||||
return readTotal
|
||||
return readTotal, nil
|
||||
}
|
||||
|
||||
func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) uint32 {
|
||||
if len(v.CurrentData) == 0 {
|
||||
v.loadSingleUnit()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return v.copy(buffer, offset, v.Position, count)
|
||||
}
|
||||
|
||||
func (v *Stream) readInternal(buffer []byte, offset, count uint32) uint32 {
|
||||
v.bufferData()
|
||||
func (v *Stream) readInternal(buffer []byte, offset, count uint32) (uint32, error) {
|
||||
if err := v.bufferData(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
localPosition := v.CurrentPosition % v.BlockSize
|
||||
bytesToCopy := d2math.MinInt32(int32(len(v.CurrentData))-int32(localPosition), int32(count))
|
||||
localPosition := v.Position % v.Size
|
||||
|
||||
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
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
copy(buffer[offset:offset+uint32(bytesToCopy)], v.CurrentData[localPosition:localPosition+uint32(bytesToCopy)])
|
||||
copy(buffer[offset:offset+bytesToCopy], v.Data[pos:pos+bytesToCopy])
|
||||
v.Position += bytesToCopy
|
||||
|
||||
v.CurrentPosition += uint32(bytesToCopy)
|
||||
|
||||
return uint32(bytesToCopy)
|
||||
return bytesToCopy, nil
|
||||
}
|
||||
|
||||
func (v *Stream) bufferData() {
|
||||
requiredBlock := v.CurrentPosition / v.BlockSize
|
||||
func (v *Stream) bufferData() (err error) {
|
||||
blockIndex := v.Position / v.Size
|
||||
|
||||
if requiredBlock == v.CurrentBlockIndex {
|
||||
return
|
||||
if blockIndex == v.Index {
|
||||
return nil
|
||||
}
|
||||
|
||||
expectedLength := d2math.Min(v.BlockTableEntry.UncompressedFileSize-(requiredBlock*v.BlockSize), v.BlockSize)
|
||||
v.CurrentData = v.loadBlock(requiredBlock, expectedLength)
|
||||
v.CurrentBlockIndex = requiredBlock
|
||||
expectedLength := d2math.Min(v.Block.UncompressedFileSize-(blockIndex*v.Size), v.Size)
|
||||
if v.Data, err = v.loadBlock(blockIndex, expectedLength); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Stream) loadSingleUnit() {
|
||||
fileData := make([]byte, v.BlockSize)
|
||||
v.Index = blockIndex
|
||||
|
||||
_, err := v.MPQData.file.Seek(int64(v.MPQData.data.HeaderSize), 0)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = v.MPQData.file.Read(fileData)
|
||||
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
|
||||
}
|
||||
|
||||
if v.BlockSize == v.BlockTableEntry.UncompressedFileSize {
|
||||
v.CurrentData = fileData
|
||||
return
|
||||
fileData := make([]byte, v.Size)
|
||||
|
||||
if _, err = v.MPQ.file.Read(fileData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.CurrentData = decompressMulti(fileData, v.BlockTableEntry.UncompressedFileSize)
|
||||
if v.Size == v.Block.UncompressedFileSize {
|
||||
v.Data = fileData
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte {
|
||||
v.Data, err = decompressMulti(fileData, v.Block.UncompressedFileSize)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) ([]byte, error) {
|
||||
var (
|
||||
offset uint32
|
||||
toRead uint32
|
||||
)
|
||||
|
||||
if v.BlockTableEntry.HasFlag(FileCompress) || v.BlockTableEntry.HasFlag(FileImplode) {
|
||||
offset = v.BlockPositions[blockIndex]
|
||||
toRead = v.BlockPositions[blockIndex+1] - offset
|
||||
if v.Block.HasFlag(FileCompress) || v.Block.HasFlag(FileImplode) {
|
||||
offset = v.Positions[blockIndex]
|
||||
toRead = v.Positions[blockIndex+1] - offset
|
||||
} else {
|
||||
offset = blockIndex * v.BlockSize
|
||||
offset = blockIndex * v.Size
|
||||
toRead = expectedLength
|
||||
}
|
||||
|
||||
offset += v.BlockTableEntry.FilePosition
|
||||
offset += v.Block.FilePosition
|
||||
data := make([]byte, toRead)
|
||||
|
||||
_, err := v.MPQData.file.Seek(int64(offset), 0)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
if _, err := v.MPQ.file.Seek(int64(offset), io.SeekStart); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
_, err = v.MPQData.file.Read(data)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
if _, err := v.MPQ.file.Read(data); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
if v.BlockTableEntry.HasFlag(FileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 {
|
||||
if v.EncryptionSeed == 0 {
|
||||
panic("Unable to determine encryption key")
|
||||
if v.Block.HasFlag(FileEncrypted) && v.Block.UncompressedFileSize > 3 {
|
||||
if v.Block.EncryptionSeed == 0 {
|
||||
return []byte{}, errors.New("unable to determine encryption key")
|
||||
}
|
||||
|
||||
decryptBytes(data, blockIndex+v.EncryptionSeed)
|
||||
decryptBytes(data, blockIndex+v.Block.EncryptionSeed)
|
||||
}
|
||||
|
||||
if v.BlockTableEntry.HasFlag(FileCompress) && (toRead != expectedLength) {
|
||||
if !v.BlockTableEntry.HasFlag(FileSingleUnit) {
|
||||
data = decompressMulti(data, expectedLength)
|
||||
} else {
|
||||
data = pkDecompress(data)
|
||||
}
|
||||
if v.Block.HasFlag(FileCompress) && (toRead != expectedLength) {
|
||||
if !v.Block.HasFlag(FileSingleUnit) {
|
||||
return decompressMulti(data, expectedLength)
|
||||
}
|
||||
|
||||
if v.BlockTableEntry.HasFlag(FileImplode) && (toRead != expectedLength) {
|
||||
data = pkDecompress(data)
|
||||
return pkDecompress(data)
|
||||
}
|
||||
|
||||
return data
|
||||
if v.Block.HasFlag(FileImplode) && (toRead != expectedLength) {
|
||||
return pkDecompress(data)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
//nolint:gomnd // Will fix enum values later
|
||||
func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte {
|
||||
func decompressMulti(data []byte /*expectedLength*/, _ uint32) ([]byte, error) {
|
||||
compressionType := data[0]
|
||||
|
||||
switch compressionType {
|
||||
case 1: // Huffman
|
||||
panic("huffman decompression not supported")
|
||||
return []byte{}, errors.New("huffman decompression not supported")
|
||||
case 2: // ZLib/Deflate
|
||||
return deflate(data[1:])
|
||||
case 8: // PKLib/Impode
|
||||
return pkDecompress(data[1:])
|
||||
case 0x10: // BZip2
|
||||
panic("bzip2 decompression not supported")
|
||||
return []byte{}, errors.New("bzip2 decompression not supported")
|
||||
case 0x80: // IMA ADPCM Stereo
|
||||
return d2compression.WavDecompress(data[1:], 2)
|
||||
return d2compression.WavDecompress(data[1:], 2), nil
|
||||
case 0x40: // IMA ADPCM Mono
|
||||
return d2compression.WavDecompress(data[1:], 1)
|
||||
return d2compression.WavDecompress(data[1:], 1), nil
|
||||
case 0x12:
|
||||
panic("lzma decompression not supported")
|
||||
return []byte{}, errors.New("lzma decompression not supported")
|
||||
// Combos
|
||||
case 0x22:
|
||||
// sparse then zlib
|
||||
panic("sparse decompression + deflate decompression not supported")
|
||||
return []byte{}, errors.New("sparse decompression + deflate decompression not supported")
|
||||
case 0x30:
|
||||
// sparse then bzip2
|
||||
panic("sparse decompression + bzip2 decompression not supported")
|
||||
return []byte{}, errors.New("sparse decompression + bzip2 decompression not supported")
|
||||
case 0x41:
|
||||
sinput := d2compression.HuffmanDecompress(data[1:])
|
||||
sinput = d2compression.WavDecompress(sinput, 1)
|
||||
@ -268,69 +256,68 @@ func decompressMulti(data []byte /*expectedLength*/, _ uint32) []byte {
|
||||
|
||||
copy(tmp, sinput)
|
||||
|
||||
return tmp
|
||||
return tmp, nil
|
||||
case 0x48:
|
||||
// byte[] result = PKDecompress(sinput, outputLength);
|
||||
// return MpqWavCompression.Decompress(new MemoryStream(result), 1);
|
||||
panic("pk + mpqwav decompression not supported")
|
||||
return []byte{}, errors.New("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
|
||||
return tmp, nil
|
||||
case 0x88:
|
||||
// byte[] result = PKDecompress(sinput, outputLength);
|
||||
// return MpqWavCompression.Decompress(new MemoryStream(result), 2);
|
||||
panic("pk + wav decompression not supported")
|
||||
default:
|
||||
panic(fmt.Sprintf("decompression not supported for unknown compression type %X", compressionType))
|
||||
}
|
||||
return []byte{}, errors.New("pk + wav decompression not supported")
|
||||
}
|
||||
|
||||
func deflate(data []byte) []byte {
|
||||
return []byte{}, fmt.Errorf("decompression not supported for unknown compression type %X", compressionType)
|
||||
}
|
||||
|
||||
func deflate(data []byte) ([]byte, error) {
|
||||
b := bytes.NewReader(data)
|
||||
|
||||
r, err := zlib.NewReader(b)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
|
||||
_, err = buffer.ReadFrom(r)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
err = r.Close()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func pkDecompress(data []byte) []byte {
|
||||
func pkDecompress(data []byte) ([]byte, error) {
|
||||
b := bytes.NewReader(data)
|
||||
r, err := blast.NewReader(b)
|
||||
|
||||
r, err := blast.NewReader(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
|
||||
_, err = buffer.ReadFrom(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if _, err = buffer.ReadFrom(r); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
err = r.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
@ -8,10 +8,9 @@ type Archive interface {
|
||||
Path() string
|
||||
Contains(string) bool
|
||||
Size() uint32
|
||||
Close()
|
||||
FileExists(fileName string) bool
|
||||
Close() error
|
||||
ReadFile(fileName string) ([]byte, error)
|
||||
ReadFileStream(fileName string) (DataStream, error)
|
||||
ReadTextFile(fileName string) (string, error)
|
||||
GetFileList() ([]string, error)
|
||||
Listfile() ([]string, error)
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package d2interface
|
||||
|
||||
import "github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
type renderCallback = func(Surface) error
|
||||
|
||||
type updateCallback = func() error
|
||||
@ -21,7 +19,7 @@ type Renderer interface {
|
||||
GetCursorPos() (int, int)
|
||||
CurrentFPS() float64
|
||||
ShowPanicScreen(message string)
|
||||
Print(target *ebiten.Image, str string) error
|
||||
PrintAt(target *ebiten.Image, str string, x, y int)
|
||||
Print(target interface{}, str string) error
|
||||
PrintAt(target interface{}, str string, x, y int)
|
||||
GetWindowSize() (int, int)
|
||||
}
|
||||
|
@ -2,11 +2,14 @@ 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
|
||||
|
@ -13,17 +13,17 @@ type Terminal interface {
|
||||
OnKeyChars(event KeyCharsEvent) bool
|
||||
Render(surface Surface) error
|
||||
Execute(command string) error
|
||||
OutputRaw(text string, category d2enum.TermCategory)
|
||||
Outputf(format string, params ...interface{})
|
||||
OutputInfof(format string, params ...interface{})
|
||||
OutputWarningf(format string, params ...interface{})
|
||||
OutputErrorf(format string, params ...interface{})
|
||||
OutputClear()
|
||||
IsVisible() bool
|
||||
Rawf(category d2enum.TermCategory, format string, params ...interface{})
|
||||
Printf(format string, params ...interface{})
|
||||
Infof(format string, params ...interface{})
|
||||
Warningf(format string, params ...interface{})
|
||||
Errorf(format string, params ...interface{})
|
||||
Clear()
|
||||
Visible() bool
|
||||
Hide()
|
||||
Show()
|
||||
BindAction(name, description string, action interface{}) error
|
||||
UnbindAction(name string) error
|
||||
Bind(name, description string, arguments []string, fn func(args []string) error) error
|
||||
Unbind(name ...string) error
|
||||
}
|
||||
|
||||
// TerminalLogger is used tomake the Terminal write out
|
||||
|
@ -37,7 +37,8 @@ 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 _, err := d2mpq.Load(path); err == nil {
|
||||
if mpq, err := d2mpq.New(path); err == nil {
|
||||
_ = mpq.Close()
|
||||
return AssetSourceMPQ
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ var _ asset.Source = &Source{}
|
||||
|
||||
// NewSource creates a new MPQ Source
|
||||
func NewSource(sourcePath string) (asset.Source, error) {
|
||||
loaded, err := d2mpq.Load(sourcePath)
|
||||
loaded, err := d2mpq.FromFile(sourcePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -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 in main menu.
|
||||
// was tested and works fine.
|
||||
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)
|
||||
"ITA": 0, // (Italian) // checked
|
||||
"JPN": 0, // (Japanese)
|
||||
"KOR": 0, // (Korean)
|
||||
"SIN": 0, //
|
||||
|
@ -193,6 +193,7 @@ 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 ---
|
||||
|
||||
@ -245,6 +246,8 @@ 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"
|
||||
|
@ -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() {
|
||||
if frameIndex >= a.GetFrameCount() || frameIndex < 0 {
|
||||
return errors.New("invalid frame index")
|
||||
}
|
||||
|
||||
|
@ -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 *ebiten.Image, str string) error {
|
||||
p.PrintAt(target, str, 0, 0)
|
||||
func (p *GlyphPrinter) Print(target interface{}, str string) error {
|
||||
p.PrintAt(target.(*ebiten.Image), 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 *ebiten.Image, str string, x, y int) {
|
||||
p.drawDebugText(target, str, x, y, false)
|
||||
func (p *GlyphPrinter) PrintAt(target interface{}, str string, x, y int) {
|
||||
p.drawDebugText(target.(*ebiten.Image), str, x, y, false)
|
||||
}
|
||||
|
||||
func (p *GlyphPrinter) drawDebugText(target *ebiten.Image, str string, ox, oy int, shadow bool) {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LogLevel determines how verbose the logging is (higher is more verbose)
|
||||
@ -51,6 +52,7 @@ func NewLogger() *Logger {
|
||||
l := &Logger{
|
||||
level: LogLevelDefault,
|
||||
colorEnabled: true,
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
|
||||
l.Writer = log.Writer()
|
||||
@ -64,6 +66,7 @@ type Logger struct {
|
||||
io.Writer
|
||||
level LogLevel
|
||||
colorEnabled bool
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// SetPrefix sets a prefix for the message.
|
||||
@ -71,11 +74,17 @@ 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
|
||||
}
|
||||
@ -85,6 +94,9 @@ 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
|
||||
}
|
||||
@ -94,10 +106,6 @@ 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)
|
||||
}
|
||||
|
||||
@ -108,10 +116,6 @@ 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)
|
||||
}
|
||||
|
||||
@ -122,10 +126,6 @@ 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,10 +136,6 @@ 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)
|
||||
}
|
||||
@ -151,10 +147,6 @@ 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)
|
||||
}
|
||||
|
||||
@ -164,7 +156,10 @@ func (l *Logger) Debugf(fmtMsg string, args ...interface{}) {
|
||||
}
|
||||
|
||||
func (l *Logger) print(level LogLevel, msg string) {
|
||||
if l == nil || l.level < level {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
if l.level < level {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
if frameIndex >= a.GetFrameCount() || frameIndex < 0 {
|
||||
return errors.New("invalid frame index")
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package d2asset
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strconv"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||
@ -409,43 +410,70 @@ func (am *AssetManager) loadDCC(path string,
|
||||
|
||||
// BindTerminalCommands binds the in-game terminal comands for the asset manager.
|
||||
func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error {
|
||||
if err := term.BindAction("assetspam", "display verbose asset manager logs", func(verbose bool) {
|
||||
if err := term.Bind("assetspam", "display verbose asset manager logs", nil, am.commandAssetSpam(term)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := term.Bind("assetstat", "display asset manager cache statistics", nil, am.commandAssetStat(term)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := term.Bind("assetclear", "clear asset manager cache", nil, am.commandAssetClear); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnbindTerminalCommands unbinds commands from the terminal
|
||||
func (am *AssetManager) UnbindTerminalCommands(term d2interface.Terminal) error {
|
||||
return term.Unbind("assetspam", "assetstat", "assetclear")
|
||||
}
|
||||
|
||||
func (am *AssetManager) commandAssetSpam(term d2interface.Terminal) func([]string) error {
|
||||
return func(args []string) error {
|
||||
verbose, err := strconv.ParseBool(args[0])
|
||||
if err != nil {
|
||||
term.Errorf("asset manager verbose invalid argument")
|
||||
return nil
|
||||
}
|
||||
|
||||
if verbose {
|
||||
term.OutputInfof("asset manager verbose logging enabled")
|
||||
term.Infof("asset manager verbose logging enabled")
|
||||
} else {
|
||||
term.OutputInfof("asset manager verbose logging disabled")
|
||||
term.Infof("asset manager verbose logging disabled")
|
||||
}
|
||||
|
||||
am.palettes.SetVerbose(verbose)
|
||||
am.fonts.SetVerbose(verbose)
|
||||
am.transforms.SetVerbose(verbose)
|
||||
am.animations.SetVerbose(verbose)
|
||||
}); err != nil {
|
||||
return err
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := term.BindAction("assetstat", "display asset manager cache statistics", func() {
|
||||
func (am *AssetManager) commandAssetStat(term d2interface.Terminal) func([]string) error {
|
||||
return func([]string) error {
|
||||
var cacheStatistics = func(c d2interface.Cache) float64 {
|
||||
const percent = 100.0
|
||||
return float64(c.GetWeight()) / float64(c.GetBudget()) * percent
|
||||
}
|
||||
|
||||
term.OutputInfof("palette cache: %f", cacheStatistics(am.palettes))
|
||||
term.OutputInfof("palette transform cache: %f", cacheStatistics(am.transforms))
|
||||
term.OutputInfof("Animation cache: %f", cacheStatistics(am.animations))
|
||||
term.OutputInfof("font cache: %f", cacheStatistics(am.fonts))
|
||||
}); err != nil {
|
||||
return err
|
||||
term.Infof("palette cache: %f", cacheStatistics(am.palettes))
|
||||
term.Infof("palette transform cache: %f", cacheStatistics(am.transforms))
|
||||
term.Infof("Animation cache: %f", cacheStatistics(am.animations))
|
||||
term.Infof("font cache: %f", cacheStatistics(am.fonts))
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := term.BindAction("assetclear", "clear asset manager cache", func() {
|
||||
func (am *AssetManager) commandAssetClear([]string) error {
|
||||
am.palettes.Clear()
|
||||
am.transforms.Clear()
|
||||
am.animations.Clear()
|
||||
am.fonts.Clear()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -9,19 +9,23 @@ import (
|
||||
)
|
||||
|
||||
// NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly
|
||||
func NewAssetManager() (*AssetManager, error) {
|
||||
loader, err := d2loader.NewLoader(d2util.LogLevelDefault)
|
||||
func NewAssetManager(logLevel d2util.LogLevel) (*AssetManager, error) {
|
||||
loader, err := d2loader.NewLoader(logLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records, err := d2records.NewRecordManager(d2util.LogLevelDebug)
|
||||
records, err := d2records.NewRecordManager(logLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger := d2util.NewLogger()
|
||||
logger.SetPrefix(logPrefix)
|
||||
logger.SetLevel(logLevel)
|
||||
|
||||
manager := &AssetManager{
|
||||
Logger: d2util.NewLogger(),
|
||||
Logger: logger,
|
||||
Loader: loader,
|
||||
tables: make([]d2tbl.TextDictionary, 0),
|
||||
animations: d2cache.CreateCache(animationBudget),
|
||||
@ -31,7 +35,5 @@ func NewAssetManager() (*AssetManager, error) {
|
||||
Records: records,
|
||||
}
|
||||
|
||||
manager.SetPrefix(logPrefix)
|
||||
|
||||
return manager, err
|
||||
}
|
||||
|
@ -2,9 +2,6 @@ package d2asset
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||
|
||||
@ -132,24 +129,11 @@ 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: frameWidth,
|
||||
height: frameHeight,
|
||||
offsetX: minX,
|
||||
offsetY: minY,
|
||||
width: dccDirection.Box.Width,
|
||||
height: dccDirection.Box.Height,
|
||||
offsetX: dccDirection.Box.Left,
|
||||
offsetY: dccDirection.Box.Top,
|
||||
decoded: true,
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package d2audio
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
||||
|
||||
@ -31,7 +32,7 @@ const originalFPS float64 = 25
|
||||
// A Sound that can be started and stopped
|
||||
type Sound struct {
|
||||
effect d2interface.SoundEffect
|
||||
entry *d2records.SoundDetailsRecord
|
||||
entry *d2records.SoundDetailRecord
|
||||
volume float64
|
||||
vTarget float64
|
||||
vRate float64
|
||||
@ -103,6 +104,11 @@ func (s *Sound) Stop() {
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the sound filename
|
||||
func (s *Sound) String() string {
|
||||
return s.entry.Handle
|
||||
}
|
||||
|
||||
// SoundEngine provides functions for playing sounds
|
||||
type SoundEngine struct {
|
||||
asset *d2asset.AssetManager
|
||||
@ -128,44 +134,26 @@ func NewSoundEngine(provider d2interface.AudioProvider,
|
||||
r.Logger.SetPrefix(logPrefix)
|
||||
r.Logger.SetLevel(l)
|
||||
|
||||
err := term.BindAction("playsoundid", "plays the sound for a given id", func(id int) {
|
||||
r.PlaySoundID(id)
|
||||
})
|
||||
if err != nil {
|
||||
if err := term.Bind("playsoundid", "plays the sound for a given id", []string{"id"}, r.commandPlaySoundID); err != nil {
|
||||
r.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
err = term.BindAction("playsound", "plays the sound for a given handle string", func(handle string) {
|
||||
r.PlaySoundHandle(handle)
|
||||
})
|
||||
if err != nil {
|
||||
if err := term.Bind("playsound", "plays the sound for a given handle string", []string{"name"}, r.commandPlaySound); err != nil {
|
||||
r.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
err = term.BindAction("activesounds", "list currently active sounds", func() {
|
||||
for s := range r.sounds {
|
||||
if err != nil {
|
||||
if err := term.Bind("activesounds", "list currently active sounds", nil, r.commandActiveSounds); err != nil {
|
||||
r.Error(err.Error())
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
r.Info(fmt.Sprint(s))
|
||||
}
|
||||
})
|
||||
|
||||
err = term.BindAction("killsounds", "kill active sounds", func() {
|
||||
for s := range r.sounds {
|
||||
if err != nil {
|
||||
if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil {
|
||||
r.Error(err.Error())
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Stop()
|
||||
}
|
||||
})
|
||||
|
||||
return &r
|
||||
}
|
||||
|
||||
@ -194,6 +182,11 @@ func (s *SoundEngine) Advance(elapsed float64) {
|
||||
}
|
||||
}
|
||||
|
||||
// UnbindTerminalCommands unbinds commands from the terminal
|
||||
func (s *SoundEngine) UnbindTerminalCommands(term d2interface.Terminal) error {
|
||||
return term.Unbind("playsoundid", "playsound", "activesounds", "killsounds")
|
||||
}
|
||||
|
||||
// Reset stop all sounds and reset state
|
||||
func (s *SoundEngine) Reset() {
|
||||
for snd := range s.sounds {
|
||||
@ -242,3 +235,35 @@ func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
|
||||
sound := s.asset.Records.Sound.Details[handle].Index
|
||||
return s.PlaySoundID(sound)
|
||||
}
|
||||
|
||||
func (s *SoundEngine) commandPlaySoundID(args []string) error {
|
||||
id, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid argument")
|
||||
}
|
||||
|
||||
s.PlaySoundID(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoundEngine) commandPlaySound(args []string) error {
|
||||
s.PlaySoundHandle(args[0])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoundEngine) commandActiveSounds([]string) error {
|
||||
for sound := range s.sounds {
|
||||
s.Info(sound.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (s *SoundEngine) commandKillSounds([]string) error {
|
||||
for sound := range s.sounds {
|
||||
sound.Stop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ type CommandRegistration struct {
|
||||
Enabled bool
|
||||
Name string
|
||||
Description string
|
||||
Callback interface{}
|
||||
Args []string
|
||||
Callback func(args []string) error
|
||||
}
|
||||
|
||||
// New creates a new CommandRegistration. By default, IsCommandRegistration is false.
|
||||
|
@ -5,8 +5,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||
)
|
||||
|
||||
// Configuration defines the configuration for the engine, loaded from config.json
|
||||
@ -21,7 +19,6 @@ type Configuration struct {
|
||||
RunInBackground bool
|
||||
VsyncEnabled bool
|
||||
Backend string
|
||||
LogLevel d2util.LogLevel
|
||||
path string
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,6 @@ import (
|
||||
"os/user"
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
||||
)
|
||||
|
||||
// DefaultConfig creates and returns a default configuration
|
||||
@ -37,7 +35,6 @@ func DefaultConfig() *Configuration {
|
||||
"d2video.mpq",
|
||||
"d2speech.mpq",
|
||||
},
|
||||
LogLevel: d2util.LogLevelDefault,
|
||||
path: DefaultConfigPath(),
|
||||
}
|
||||
|
||||
|
@ -74,8 +74,7 @@ func NewBox(
|
||||
renderer d2interface.Renderer,
|
||||
ui *d2ui.UIManager,
|
||||
contentLayout *Layout,
|
||||
width, height int,
|
||||
x, y int,
|
||||
width, height, x, y int,
|
||||
l d2util.LogLevel,
|
||||
title string,
|
||||
) *Box {
|
||||
|
@ -1,8 +1,6 @@
|
||||
package d2gui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
|
||||
)
|
||||
@ -37,28 +35,3 @@ 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
|
||||
}
|
||||
|
@ -248,16 +248,16 @@ func (l *Layout) renderEntryDebug(entry *layoutEntry, target d2interface.Surface
|
||||
target.PushTranslation(entry.x, entry.y)
|
||||
defer target.Pop()
|
||||
|
||||
drawColor := rgbaColor(white)
|
||||
drawColor := d2util.Color(white)
|
||||
switch entry.widget.(type) {
|
||||
case *Layout:
|
||||
drawColor = rgbaColor(magenta)
|
||||
drawColor = d2util.Color(magenta)
|
||||
case *SpacerStatic, *SpacerDynamic:
|
||||
drawColor = rgbaColor(grey2)
|
||||
drawColor = d2util.Color(grey2)
|
||||
case *Label:
|
||||
drawColor = rgbaColor(green)
|
||||
drawColor = d2util.Color(green)
|
||||
case *Button:
|
||||
drawColor = rgbaColor(yellow)
|
||||
drawColor = d2util.Color(yellow)
|
||||
}
|
||||
|
||||
target.DrawLine(entry.width, 0, drawColor)
|
||||
@ -487,7 +487,7 @@ func (l *Layout) createButton(renderer d2interface.Renderer, text string,
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
textColor := rgbaColor(grey)
|
||||
textColor := d2util.Color(grey)
|
||||
textWidth, textHeight := font.GetTextMetrics(text)
|
||||
textX := half(buttonWidth) - half(textWidth)
|
||||
textY := half(buttonHeight) - half(textHeight) + config.textOffset
|
||||
|
@ -64,10 +64,7 @@ const (
|
||||
)
|
||||
|
||||
// NewLayoutScrollbar attaches a scrollbar to the parentLayout to control the targetLayout
|
||||
func NewLayoutScrollbar(
|
||||
parentLayout *Layout,
|
||||
targetLayout *Layout,
|
||||
) *LayoutScrollbar {
|
||||
func NewLayoutScrollbar(parentLayout, targetLayout *Layout) *LayoutScrollbar {
|
||||
parentW, parentH := parentLayout.GetSize()
|
||||
_, targetH := targetLayout.GetSize()
|
||||
gutterHeight := parentH - (2 * textSliderPartHeight)
|
||||
|
@ -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.CharStatsRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) {
|
||||
func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) {
|
||||
baseSkills := map[int]*HeroSkill{}
|
||||
|
||||
for idx := range classStats.BaseSkill {
|
||||
|
@ -10,32 +10,27 @@ type HeroStatsState struct {
|
||||
Level int `json:"level"`
|
||||
Experience int `json:"experience"`
|
||||
|
||||
Vitality int `json:"vitality"`
|
||||
Energy int `json:"energy"`
|
||||
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"`
|
||||
|
||||
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"`
|
||||
|
||||
FireResistance int `json:"fireResistance"`
|
||||
ColdResistance int `json:"coldResistance"`
|
||||
LightningResistance int `json:"lightningResistance"`
|
||||
PoisonResistance int `json:"poisonResistance"`
|
||||
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
|
||||
MaxStamina int `json:"maxStamina"`
|
||||
|
||||
// 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.CharStatsRecord) *HeroStatsState {
|
||||
func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2records.CharStatRecord) *HeroStatsState {
|
||||
result := HeroStatsState{
|
||||
Level: 1,
|
||||
Experience: 0,
|
||||
@ -44,6 +39,8 @@ 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,
|
||||
|
@ -136,7 +136,6 @@ 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)
|
||||
|
@ -277,7 +277,7 @@ var itemStatCosts = map[string]*d2records.ItemStatCostRecord{
|
||||
}
|
||||
|
||||
// nolint:gochecknoglobals // just a test
|
||||
var charStats = map[d2enum.Hero]*d2records.CharStatsRecord{
|
||||
var charStats = map[d2enum.Hero]*d2records.CharStatRecord{
|
||||
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.MonStatsRecord{
|
||||
var monStats = map[string]*d2records.MonStatRecord{
|
||||
"Specter": {NameString: "Specter", ID: 40},
|
||||
}
|
||||
|
||||
|
@ -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, len(m.dt1TileData))
|
||||
tiles := make([]d2dt1.Tile, 0)
|
||||
|
||||
for idx := range m.dt1TileData {
|
||||
if m.dt1TileData[idx].Style != int32(style) || m.dt1TileData[idx].Sequence != int32(sequence) ||
|
||||
|
@ -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 int, gold int) *Player {
|
||||
leftSkill, rightSkill, 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.MonStatsRecord, direction int) (*NPC, error) {
|
||||
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatRecord, direction int) (*NPC, error) {
|
||||
// https://github.com/OpenDiablo2/OpenDiablo2/issues/803
|
||||
result := &NPC{
|
||||
mapEntity: newMapEntity(x, y),
|
||||
@ -237,7 +237,6 @@ func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.Ove
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// https://github.com/OpenDiablo2/OpenDiablo2/issues/767
|
||||
animation.Rewind()
|
||||
animation.ResetPlayedCount()
|
||||
|
||||
@ -263,7 +262,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.ObjectDetailsRecord,
|
||||
func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailRecord,
|
||||
palettePath string) (*Object, error) {
|
||||
locX, locY := float64(x), float64(y)
|
||||
entity := &Object{
|
||||
|
@ -22,8 +22,8 @@ type NPC struct {
|
||||
action int
|
||||
path int
|
||||
repetitions int
|
||||
monstatRecord *d2records.MonStatsRecord
|
||||
monstatEx *d2records.MonStats2Record
|
||||
monstatRecord *d2records.MonStatRecord
|
||||
monstatEx *d2records.MonStat2Record
|
||||
HasPaths bool
|
||||
isDone bool
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ type Object struct {
|
||||
composite *d2asset.Composite
|
||||
highlight bool
|
||||
// nameLabel d2ui.Label
|
||||
objectRecord *d2records.ObjectDetailsRecord
|
||||
objectRecord *d2records.ObjectDetailRecord
|
||||
drawLayer int
|
||||
name string
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ package d2maprenderer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
|
||||
@ -86,20 +88,11 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
|
||||
result.Camera.position = &startPosition
|
||||
result.viewport.SetCamera(&result.Camera)
|
||||
|
||||
var err error
|
||||
err = term.BindAction("mapdebugvis", "set map debug visualization level", func(level int) {
|
||||
result.mapDebugVisLevel = level
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if err := term.Bind("mapdebugvis", "set map debug visualization level", nil, result.commandMapDebugVis); err != nil {
|
||||
result.Errorf("could not bind the mapdebugvis action, err: %v", err)
|
||||
}
|
||||
|
||||
err = term.BindAction("entitydebugvis", "set entity debug visualization level", func(level int) {
|
||||
result.entityDebugVisLevel = level
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if err := term.Bind("entitydebugvis", "set entity debug visualization level", nil, result.commandEntityDebugVis); err != nil {
|
||||
result.Errorf("could not bind the entitydebugvis action, err: %v", err)
|
||||
}
|
||||
|
||||
@ -110,6 +103,33 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
|
||||
return result
|
||||
}
|
||||
|
||||
// UnbindTerminalCommands unbinds commands from the terminal
|
||||
func (mr *MapRenderer) UnbindTerminalCommands(term d2interface.Terminal) error {
|
||||
return term.Unbind("mapdebugvis", "entitydebugvis")
|
||||
}
|
||||
|
||||
func (mr *MapRenderer) commandMapDebugVis(args []string) error {
|
||||
level, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid argument supplied")
|
||||
}
|
||||
|
||||
mr.mapDebugVisLevel = level
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mr *MapRenderer) commandEntityDebugVis(args []string) error {
|
||||
level, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid argument supplied")
|
||||
}
|
||||
|
||||
mr.entityDebugVisLevel = level
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegenerateTileCache calls MapRenderer.generateTileCache().
|
||||
func (mr *MapRenderer) RegenerateTileCache() {
|
||||
mr.generateTileCache()
|
||||
|
@ -22,7 +22,7 @@ func armorTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Animation.Token.Armor = records
|
||||
|
||||
r.Logger.Infof("Loaded %d ArmorType records", len(records))
|
||||
r.Debugf("Loaded %d ArmorType records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ func autoMagicLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d AutoMagic records", len(records))
|
||||
r.Debugf("Loaded %d AutoMagic records", len(records))
|
||||
|
||||
r.Item.AutoMagic = records
|
||||
|
||||
|
@ -37,7 +37,7 @@ func autoMapLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d AutoMapRecord records", len(records))
|
||||
r.Debugf("Loaded %d AutoMap records", len(records))
|
||||
|
||||
r.Level.AutoMaps = records
|
||||
|
||||
|
@ -102,7 +102,7 @@ func beltsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d belts", len(records))
|
||||
r.Debugf("Loaded %d Belt records", len(records))
|
||||
|
||||
r.Item.Belts = records
|
||||
|
||||
|
@ -19,7 +19,7 @@ func bodyLocationsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
panic(d.Err)
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d Body Location records", len(records))
|
||||
r.Debugf("Loaded %d BodyLocation records", len(records))
|
||||
|
||||
r.BodyLocations = records
|
||||
|
||||
|
@ -8,7 +8,7 @@ func booksLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
records := make(Books)
|
||||
|
||||
for d.Next() {
|
||||
record := &BooksRecord{
|
||||
record := &BookRecord{
|
||||
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.Logger.Infof("Loaded %d book items", len(records))
|
||||
r.Debugf("Loaded %d Book records", len(records))
|
||||
|
||||
r.Item.Books = records
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package d2records
|
||||
|
||||
// Books stores all of the BooksRecords
|
||||
type Books map[string]*BooksRecord
|
||||
// Books stores all of the BookRecords
|
||||
type Books map[string]*BookRecord
|
||||
|
||||
// BooksRecord is a representation of a row from books.txt
|
||||
type BooksRecord struct {
|
||||
// BookRecord is a representation of a row from books.txt
|
||||
type BookRecord struct {
|
||||
Name string
|
||||
Namco string // The displayed name, where the string prefix is "Tome"
|
||||
Completed string
|
||||
|
@ -5,32 +5,28 @@ import (
|
||||
)
|
||||
|
||||
func skillCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
records, err := loadCalculations(r, d)
|
||||
records, err := loadCalculations(r, d, "Skill")
|
||||
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)
|
||||
records, err := loadCalculations(r, d, "Missile")
|
||||
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) (Calculations, error) {
|
||||
func loadCalculations(r *RecordManager, d *d2txt.DataDictionary, name string) (Calculations, error) {
|
||||
records := make(Calculations)
|
||||
|
||||
for d.Next() {
|
||||
@ -45,7 +41,7 @@ func loadCalculations(r *RecordManager, d *d2txt.DataDictionary) (Calculations,
|
||||
return nil, d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d Skill Calculation records", len(records))
|
||||
r.Debugf("Loaded %d %s Calculation records", len(records), name)
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ func charStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
}
|
||||
|
||||
for d.Next() {
|
||||
record := &CharStatsRecord{
|
||||
record := &CharStatRecord{
|
||||
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.Logger.Infof("Loaded %d CharStats records", len(records))
|
||||
r.Debugf("Loaded %d CharStat records", len(records))
|
||||
|
||||
r.Character.Stats = records
|
||||
|
||||
|
@ -2,11 +2,11 @@ package d2records
|
||||
|
||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
|
||||
// CharStats holds all of the CharStatsRecords
|
||||
type CharStats map[d2enum.Hero]*CharStatsRecord
|
||||
// CharStats holds all of the CharStatRecords
|
||||
type CharStats map[d2enum.Hero]*CharStatRecord
|
||||
|
||||
// CharStatsRecord is a struct that represents a single row from charstats.txt
|
||||
type CharStatsRecord struct {
|
||||
// CharStatRecord is a struct that represents a single row from charstats.txt
|
||||
type CharStatRecord struct {
|
||||
Class d2enum.Hero
|
||||
|
||||
// the initial stats at character level 1
|
||||
|
@ -22,7 +22,7 @@ func colorsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Colors = records
|
||||
|
||||
r.Logger.Infof("Loaded %d Color records", len(records))
|
||||
r.Debugf("Loaded %d Color records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ func componentCodesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d ComponentCode records", len(records))
|
||||
r.Debugf("Loaded %d ComponentCode records", len(records))
|
||||
|
||||
r.ComponentCodes = records
|
||||
|
||||
|
@ -22,7 +22,7 @@ func compositeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Animation.Token.Composite = records
|
||||
|
||||
r.Logger.Infof("Loaded %d Composite Type records", len(records))
|
||||
r.Debugf("Loaded %d CompositeType records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ func cubeModifierLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Item.Cube.Modifiers = records
|
||||
|
||||
r.Logger.Infof("Loaded %d Cube Modifier records", len(records))
|
||||
r.Debugf("Loaded %d CubeModifier records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ func cubeTypeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Item.Cube.Types = records
|
||||
|
||||
r.Logger.Infof("Loaded %d Cube Type records", len(records))
|
||||
r.Debugf("Loaded %d CubeType records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ func cubeRecipeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d CubeMainRecord records", len(records))
|
||||
r.Debugf("Loaded %d CubeRecipe records", len(records))
|
||||
|
||||
r.Item.Cube.Recipes = records
|
||||
|
||||
|
@ -42,7 +42,7 @@ func difficultyLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d DifficultyLevel records", len(records))
|
||||
r.Debugf("Loaded %d DifficultyLevel records", len(records))
|
||||
|
||||
r.DifficultyLevels = records
|
||||
|
||||
|
@ -20,7 +20,7 @@ func elemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d ElemType records", len(records))
|
||||
r.Debugf("Loaded %d ElemType records", len(records))
|
||||
|
||||
r.ElemTypes = records
|
||||
|
||||
|
@ -20,7 +20,7 @@ func eventsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d Event records", len(records))
|
||||
r.Debugf("Loaded %d Event records", len(records))
|
||||
|
||||
r.Character.Events = records
|
||||
|
||||
|
@ -48,7 +48,7 @@ func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
}
|
||||
|
||||
for d.Next() {
|
||||
record := &ExperienceBreakpointsRecord{
|
||||
record := &ExperienceBreakpointRecord{
|
||||
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.Logger.Infof("Loaded %d Experience Breakpoint records", len(breakpoints))
|
||||
r.Debugf("Loaded %d ExperienceBreakpoint records", len(breakpoints))
|
||||
|
||||
r.Character.MaxLevel = maxLevels
|
||||
r.Character.Experience = breakpoints
|
||||
|
@ -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]*ExperienceBreakpointsRecord
|
||||
type ExperienceBreakpoints map[int]*ExperienceBreakpointRecord
|
||||
|
||||
// ExperienceMaxLevels defines the max character levels
|
||||
type ExperienceMaxLevels map[d2enum.Hero]int
|
||||
|
||||
// ExperienceBreakpointsRecord describes the experience points required to
|
||||
// ExperienceBreakpointRecord describes the experience points required to
|
||||
// gain a level for all character classes
|
||||
type ExperienceBreakpointsRecord struct {
|
||||
type ExperienceBreakpointRecord struct {
|
||||
Level int
|
||||
HeroBreakpoints map[d2enum.Hero]int
|
||||
Ratio int
|
||||
|
@ -19,7 +19,7 @@ func gambleLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d gamble records", len(records))
|
||||
r.Debugf("Loaded %d Gamble records", len(records))
|
||||
|
||||
r.Gamble = records
|
||||
|
||||
|
@ -4,12 +4,12 @@ import (
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||
)
|
||||
|
||||
// LoadGems loads gem records into a map[string]*GemsRecord
|
||||
// LoadGems loads gem records into a map[string]*GemRecord
|
||||
func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
records := make(Gems)
|
||||
|
||||
for d.Next() {
|
||||
gem := &GemsRecord{
|
||||
gem := &GemRecord{
|
||||
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.Logger.Infof("Loaded %d Gems records", len(records))
|
||||
r.Debugf("Loaded %d Gem records", len(records))
|
||||
|
||||
r.Item.Gems = records
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
package d2records
|
||||
|
||||
// Gems stores all of the GemsRecords
|
||||
type Gems map[string]*GemsRecord
|
||||
// Gems stores all of the GemRecords
|
||||
type Gems map[string]*GemRecord
|
||||
|
||||
// GemsRecord is a representation of a single row of gems.txt
|
||||
// GemRecord is a representation of a single row of gems.txt
|
||||
// it describes the properties of socketable items
|
||||
type GemsRecord struct {
|
||||
type GemRecord struct {
|
||||
Name string
|
||||
Letter string
|
||||
Transform int
|
||||
|
@ -22,7 +22,7 @@ func hirelingDescriptionLoader(r *RecordManager, d *d2txt.DataDictionary) error
|
||||
|
||||
r.Hireling.Descriptions = records
|
||||
|
||||
r.Logger.Infof("Loaded %d Hireling Descriptions records", len(records))
|
||||
r.Debugf("Loaded %d HirelingDescription records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func hirelingLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d Hireling records", len(records))
|
||||
r.Debugf("Loaded %d Hireling records", len(records))
|
||||
|
||||
r.Hireling.Details = records
|
||||
|
||||
|
@ -22,7 +22,7 @@ func hitClassLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Animation.Token.HitClass = records
|
||||
|
||||
r.Logger.Infof("Loaded %d HitClass records", len(records))
|
||||
r.Debugf("Loaded %d HitClass records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ func inventoryLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d Inventory Panel records", len(records))
|
||||
r.Debugf("Loaded %d Inventory records", len(records))
|
||||
|
||||
r.Layout.Inventory = records
|
||||
|
||||
|
@ -70,7 +70,7 @@ func loadAffixDictionary(
|
||||
}
|
||||
|
||||
name := getAffixString(superType, subType)
|
||||
r.Logger.Infof("Loaded %d %s records", len(records), name)
|
||||
r.Debugf("Loaded %d %s records", len(records), name)
|
||||
|
||||
return records, groups, nil
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ func armorLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d armors", len(records))
|
||||
r.Debugf("Loaded %d Armor Item records", len(records))
|
||||
|
||||
r.Item.Armors = records
|
||||
|
||||
|
@ -21,7 +21,7 @@ func lowQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Item.LowQualityPrefixes = records
|
||||
|
||||
r.Logger.Infof("Loaded %d Low Item Quality records", len(records))
|
||||
r.Debugf("Loaded %d LowQuality records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ func miscItemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d misc items", len(records))
|
||||
r.Debugf("Loaded %d Misc Item records", len(records))
|
||||
|
||||
r.Item.Misc = records
|
||||
|
||||
|
@ -45,7 +45,7 @@ func itemQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
|
||||
r.Item.Quality = records
|
||||
|
||||
r.Logger.Infof("Loaded %d ItemQualities records", len(records))
|
||||
r.Debugf("Loaded %d ItemQuality records", len(records))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ func itemRatioLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d ItemRatio records", len(records))
|
||||
r.Debugf("Loaded %d ItemRatio records", len(records))
|
||||
|
||||
r.Item.Ratios = records
|
||||
|
||||
|
@ -76,7 +76,7 @@ func itemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d ItemType records", len(records))
|
||||
r.Debugf("Loaded %d ItemType records", len(records))
|
||||
|
||||
r.Item.Types = records
|
||||
r.Item.Equivalency = equivMap
|
||||
|
@ -13,7 +13,7 @@ func weaponsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d weapons", len(records))
|
||||
r.Debugf("Loaded %d Weapon records", len(records))
|
||||
|
||||
r.Item.Weapons = records
|
||||
|
||||
|
@ -95,7 +95,7 @@ func itemStatCostLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d ItemStatCost records", len(records))
|
||||
r.Debugf("Loaded %d ItemStatCost records", len(records))
|
||||
|
||||
r.Item.Stats = records
|
||||
|
||||
|
@ -11,7 +11,7 @@ func levelDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
records := make(LevelDetails)
|
||||
|
||||
for d.Next() {
|
||||
record := &LevelDetailsRecord{
|
||||
record := &LevelDetailRecord{
|
||||
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.Logger.Infof("Loaded %d LevelDetails records", len(records))
|
||||
r.Debugf("Loaded %d LevelDetail records", len(records))
|
||||
|
||||
r.Level.Details = records
|
||||
|
||||
|
@ -2,13 +2,13 @@ package d2records
|
||||
|
||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
|
||||
// LevelDetails has all of the LevelDetailsRecords
|
||||
type LevelDetails map[int]*LevelDetailsRecord
|
||||
// LevelDetails has all of the LevelDetailRecords
|
||||
type LevelDetails map[int]*LevelDetailRecord
|
||||
|
||||
// LevelDetailsRecord is a representation of a row from levels.txt
|
||||
// LevelDetailRecord is a representation of a row from levels.txt
|
||||
// it describes lots of things about the levels, like where they are connected,
|
||||
// what kinds of monsters spawn, the level generator type, and lots of other stuff.
|
||||
type LevelDetailsRecord struct {
|
||||
type LevelDetailRecord struct {
|
||||
|
||||
// Name
|
||||
// This column has no function, it only serves as a comment field to make it
|
||||
|
@ -8,7 +8,7 @@ func levelMazeDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
records := make(LevelMazeDetails)
|
||||
|
||||
for d.Next() {
|
||||
record := &LevelMazeDetailsRecord{
|
||||
record := &LevelMazeDetailRecord{
|
||||
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.Logger.Infof("Loaded %d LevelMazeDetails records", len(records))
|
||||
r.Debugf("Loaded %d LevelMazeDetail records", len(records))
|
||||
|
||||
r.Level.Maze = records
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
package d2records
|
||||
|
||||
// LevelMazeDetails stores all of the LevelMazeDetailsRecords
|
||||
type LevelMazeDetails map[int]*LevelMazeDetailsRecord
|
||||
// LevelMazeDetails stores all of the LevelMazeDetailRecords
|
||||
type LevelMazeDetails map[int]*LevelMazeDetailRecord
|
||||
|
||||
// LevelMazeDetailsRecord is a representation of a row from lvlmaze.txt
|
||||
// LevelMazeDetailRecord is a representation of a row from lvlmaze.txt
|
||||
// these records define the parameters passed to the maze level generator
|
||||
type LevelMazeDetailsRecord struct {
|
||||
type LevelMazeDetailRecord struct {
|
||||
// descriptive, not loaded in game. Corresponds with Name field in
|
||||
// Levels.txt
|
||||
Name string // Name
|
||||
|
@ -42,7 +42,7 @@ func levelPresetLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
records[record.DefinitionID] = record
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d level presets", len(records))
|
||||
r.Debugf("Loaded %d LevelPresets records", len(records))
|
||||
|
||||
if d.Err != nil {
|
||||
return d.Err
|
||||
|
@ -40,7 +40,7 @@ func levelSubstitutionsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d LevelSubstitution records", len(records))
|
||||
r.Debugf("Loaded %d LevelSubstitution records", len(records))
|
||||
|
||||
r.Level.Sub = records
|
||||
|
||||
|
@ -58,7 +58,7 @@ func levelTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d LevelType records", len(records))
|
||||
r.Debugf("Loaded %d LevelType records", len(records))
|
||||
|
||||
r.Level.Types = records
|
||||
|
||||
|
@ -30,7 +30,7 @@ func levelWarpsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d level warps", len(records))
|
||||
r.Debugf("Loaded %d LevelWarp records", len(records))
|
||||
|
||||
r.Level.Warp = records
|
||||
|
||||
|
@ -304,7 +304,7 @@ func missilesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d Missile Records", len(records))
|
||||
r.Debugf("Loaded %d Missile records", len(records))
|
||||
|
||||
r.Missiles = records
|
||||
|
||||
|
@ -19,7 +19,7 @@ func monsterAiLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d MonsterAI records", len(records))
|
||||
r.Debugf("Loaded %d MonsterAI records", len(records))
|
||||
|
||||
r.Monster.AI = records
|
||||
|
||||
|
@ -49,7 +49,7 @@ func monsterEquipmentLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
length += len(records[k])
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d MonsterEquipment records", length)
|
||||
r.Debugf("Loaded %d MonsterEquipment records", length)
|
||||
|
||||
r.Monster.Equipment = records
|
||||
|
||||
|
@ -52,7 +52,7 @@ func monsterLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d MonsterLevel records", len(records))
|
||||
r.Debugf("Loaded %d MonsterLevel records", len(records))
|
||||
|
||||
r.Monster.Levels = records
|
||||
|
||||
|
@ -21,7 +21,7 @@ func monsterModeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
r.Logger.Infof("Loaded %d MonMode records", len(records))
|
||||
r.Debugf("Loaded %d MonMode records", len(records))
|
||||
|
||||
r.Monster.Modes = records
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package d2records
|
||||
|
||||
// MonModes stores all of the GemsRecords
|
||||
// MonModes stores all of the MonModeRecords
|
||||
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
Loading…
Reference in New Issue
Block a user