mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-02 14:46:28 -05:00
commit
31d8343fd6
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
|
||||
|
304
d2app/app.go
304
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,17 +85,10 @@ 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{}
|
||||
Debug *bool
|
||||
profiler *string
|
||||
Server *d2networking.ServerOptions
|
||||
LogLevel *d2util.LogLevel
|
||||
}
|
||||
|
||||
const (
|
||||
@ -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)
|
||||
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
|
||||
return int16(v.GetUInt16())
|
||||
}
|
||||
|
||||
for offset := uint64(0); offset < bytesPerInt16; offset++ {
|
||||
shift := uint8(bitsPerByte * offset)
|
||||
result += int16(v.data[v.position+offset]) << shift
|
||||
}
|
||||
// GetUInt16 returns a uint16 word from the stream
|
||||
//nolint
|
||||
func (v *StreamReader) GetUInt16() uint16 {
|
||||
b := v.ReadBytes(2)
|
||||
return uint16(b[0]) | uint16(b[1])<<8
|
||||
}
|
||||
|
||||
v.position += bytesPerInt16
|
||||
// GetInt32 returns an int32 dword from the stream
|
||||
func (v *StreamReader) GetInt32() int32 {
|
||||
return int32(v.GetUInt32())
|
||||
}
|
||||
|
||||
return result
|
||||
// GetUInt32 returns a uint32 dword from the stream
|
||||
//nolint
|
||||
func (v *StreamReader) GetUInt32() uint32 {
|
||||
b := v.ReadBytes(4)
|
||||
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||
}
|
||||
|
||||
// GetInt64 returns a uint64 qword from the stream
|
||||
func (v *StreamReader) GetInt64() int64 {
|
||||
return int64(v.GetUInt64())
|
||||
}
|
||||
|
||||
// GetUInt64 returns a uint64 qword from the stream
|
||||
//nolint
|
||||
func (v *StreamReader) GetUInt64() uint64 {
|
||||
b := v.ReadBytes(8)
|
||||
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
|
||||
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
|
||||
}
|
||||
|
||||
// GetPosition returns the current stream position
|
||||
func (v *StreamReader) GetPosition() uint64 {
|
||||
return v.position
|
||||
}
|
||||
|
||||
// SetPosition sets the stream position with the given position
|
||||
@ -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"
|
||||
@ -19,33 +18,11 @@ var _ d2interface.Archive = &MPQ{} // Static check to confirm struct conforms to
|
||||
|
||||
// MPQ represents an MPQ archive
|
||||
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
|
||||
filePath string
|
||||
file *os.File
|
||||
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!")
|
||||
blockPosSize := blockPositionCount << 2 //nolint:gomnd // MPQ magic
|
||||
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
|
||||
}
|
||||
|
||||
v.Index = blockIndex
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Stream) loadSingleUnit() {
|
||||
fileData := make([]byte, v.BlockSize)
|
||||
|
||||
_, err := v.MPQData.file.Seek(int64(v.MPQData.data.HeaderSize), 0)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
func (v *Stream) loadSingleUnit() (err error) {
|
||||
if _, err = v.MPQ.file.Seek(int64(v.MPQ.header.HeaderSize), io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = v.MPQData.file.Read(fileData)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
fileData := make([]byte, v.Size)
|
||||
|
||||
if _, err = v.MPQ.file.Read(fileData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v.BlockSize == v.BlockTableEntry.UncompressedFileSize {
|
||||
v.CurrentData = fileData
|
||||
return
|
||||
if v.Size == v.Block.UncompressedFileSize {
|
||||
v.Data = fileData
|
||||
return nil
|
||||
}
|
||||
|
||||
v.CurrentData = decompressMulti(fileData, v.BlockTableEntry.UncompressedFileSize)
|
||||
v.Data, err = decompressMulti(fileData, v.Block.UncompressedFileSize)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte {
|
||||
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)
|
||||
}
|
||||
|
||||
return pkDecompress(data)
|
||||
}
|
||||
|
||||
if v.BlockTableEntry.HasFlag(FileImplode) && (toRead != expectedLength) {
|
||||
data = pkDecompress(data)
|
||||
if v.Block.HasFlag(FileImplode) && (toRead != expectedLength) {
|
||||
return pkDecompress(data)
|
||||
}
|
||||
|
||||
return data
|
||||
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")
|
||||
}
|
||||
|
||||
return []byte{}, fmt.Errorf("decompression not supported for unknown compression type %X", compressionType)
|
||||
}
|
||||
|
||||
func deflate(data []byte) []byte {
|
||||
func deflate(data []byte) ([]byte, error) {
|
||||
b := bytes.NewReader(data)
|
||||
|
||||
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 ---
|
||||
|
||||
@ -243,16 +244,18 @@ const (
|
||||
MinipanelSmall = "/data/global/ui/PANEL/minipanel_s.dc6"
|
||||
MinipanelButton = "/data/global/ui/PANEL/minipanelbtn.DC6"
|
||||
|
||||
Frame = "/data/global/ui/PANEL/800borderframe.dc6"
|
||||
InventoryCharacterPanel = "/data/global/ui/PANEL/invchar6.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"
|
||||
SkillsPanelDruid = "/data/global/ui/SPELLS/skltree_d_back.DC6"
|
||||
SkillsPanelAssassin = "/data/global/ui/SPELLS/skltree_i_back.DC6"
|
||||
SkillsPanelNecromancer = "/data/global/ui/SPELLS/skltree_n_back.DC6"
|
||||
SkillsPanelPaladin = "/data/global/ui/SPELLS/skltree_p_back.DC6"
|
||||
SkillsPanelSorcerer = "/data/global/ui/SPELLS/skltree_s_back.DC6"
|
||||
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"
|
||||
SkillsPanelDruid = "/data/global/ui/SPELLS/skltree_d_back.DC6"
|
||||
SkillsPanelAssassin = "/data/global/ui/SPELLS/skltree_i_back.DC6"
|
||||
SkillsPanelNecromancer = "/data/global/ui/SPELLS/skltree_n_back.DC6"
|
||||
SkillsPanelPaladin = "/data/global/ui/SPELLS/skltree_p_back.DC6"
|
||||
SkillsPanelSorcerer = "/data/global/ui/SPELLS/skltree_s_back.DC6"
|
||||
|
||||
GenericSkills = "/data/global/ui/SPELLS/Skillicon.DC6"
|
||||
AmazonSkills = "/data/global/ui/SPELLS/AmSkillicon.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
|
||||
}
|
||||
|
||||
if err := term.BindAction("assetstat", "display asset manager cache statistics", func() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AssetManager) commandAssetStat(term d2interface.Terminal) func([]string) error {
|
||||
return func([]string) error {
|
||||
var cacheStatistics = func(c d2interface.Cache) float64 {
|
||||
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))
|
||||
|
||||
if err := term.BindAction("assetclear", "clear asset manager cache", func() {
|
||||
am.palettes.Clear()
|
||||
am.transforms.Clear()
|
||||
am.animations.Clear()
|
||||
am.fonts.Clear()
|
||||
}); err != nil {
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AssetManager) commandAssetClear([]string) error {
|
||||
am.palettes.Clear()
|
||||
am.transforms.Clear()
|
||||
am.animations.Clear()
|
||||
am.fonts.Clear()
|
||||
|
||||
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
|
||||
@ -73,7 +74,7 @@ func (s *Sound) SetPan(pan float64) {
|
||||
|
||||
// Play the sound
|
||||
func (s *Sound) Play() {
|
||||
s.Info("starting sound" + s.entry.Handle)
|
||||
s.Info("starting sound " + s.entry.Handle)
|
||||
s.effect.Play()
|
||||
|
||||
if s.entry.FadeIn != 0 {
|
||||
@ -103,6 +104,11 @@ func (s *Sound) Stop() {
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the sound filename
|
||||
func (s *Sound) String() string {
|
||||
return s.entry.Handle
|
||||
}
|
||||
|
||||
// SoundEngine provides functions for playing sounds
|
||||
type SoundEngine struct {
|
||||
asset *d2asset.AssetManager
|
||||
@ -128,43 +134,25 @@ 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 {
|
||||
r.Error(err.Error())
|
||||
return
|
||||
}
|
||||
if err := term.Bind("activesounds", "list currently active sounds", nil, r.commandActiveSounds); err != nil {
|
||||
r.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
r.Info(fmt.Sprint(s))
|
||||
}
|
||||
})
|
||||
|
||||
err = term.BindAction("killsounds", "kill active sounds", func() {
|
||||
for s := range r.sounds {
|
||||
if err != nil {
|
||||
r.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.Stop()
|
||||
}
|
||||
})
|
||||
if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil {
|
||||
r.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
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,8 +35,7 @@ func DefaultConfig() *Configuration {
|
||||
"d2video.mpq",
|
||||
"d2speech.mpq",
|
||||
},
|
||||
LogLevel: d2util.LogLevelDefault,
|
||||
path: DefaultConfigPath(),
|
||||
path: DefaultConfigPath(),
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
|
@ -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"`
|
||||
Health int `json:"health"`
|
||||
MaxHealth int `json:"maxHealth"`
|
||||
Mana int `json:"mana"`
|
||||
MaxMana int `json:"maxMana"`
|
||||
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
|
||||
MaxStamina int `json:"maxStamina"`
|
||||
|
||||
// values which are not saved/loaded(computed)
|
||||
Stamina float64 `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world
|
||||
NextLevelExp int `json:"-"`
|
||||
NextLevelExp int `json:"-"`
|
||||
}
|
||||
|
||||
// CreateHeroStatsState generates a running state from a hero stats.
|
||||
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