From 361b60febf77ce052fb608abdd5ee90a680ca79f Mon Sep 17 00:00:00 2001 From: Tim Sarbin Date: Thu, 24 Oct 2019 09:31:59 -0400 Subject: [PATCH] Migrated to golang codebase. --- .gitattributes | 5 - .gitignore | 6 +- .gitmodules | 0 App/OpenDiablo2.go | 33 + OpenDiablo2/Common/SceneInterface.go | 12 + OpenDiablo2/CryptoBuff.go | 20 + OpenDiablo2/Engine.go | 212 + OpenDiablo2/MPQ.go | 256 + OpenDiablo2/MPQFont.go | 34 + OpenDiablo2/MPQStream.go | 229 + OpenDiablo2/Math.go | 31 + OpenDiablo2/Palette.go | 26 + OpenDiablo2/ResourcePaths/ResourcePaths.go | 247 + OpenDiablo2/SceneMainMenu.go | 86 + OpenDiablo2/SoundEntry.go | 67 + OpenDiablo2/Sprite.go | 181 + OpenDiablo2/StringUtils.go | 36 + OpenDiablo2/UILabel.go | 80 + config.json | 20 + src/CMakeLists.txt | 44 - src/ExtraUtils/CLI11/CLI11.hpp | 4641 ----------------- src/ExtraUtils/spdlog/async.h | 87 - src/ExtraUtils/spdlog/async_logger.h | 73 - src/ExtraUtils/spdlog/common.h | 246 - .../spdlog/details/async_logger_impl.h | 110 - src/ExtraUtils/spdlog/details/circular_q.h | 72 - .../spdlog/details/console_globals.h | 74 - src/ExtraUtils/spdlog/details/file_helper.h | 152 - src/ExtraUtils/spdlog/details/fmt_helper.h | 122 - src/ExtraUtils/spdlog/details/log_msg.h | 55 - src/ExtraUtils/spdlog/details/logger_impl.h | 435 -- .../spdlog/details/mpmc_blocking_q.h | 121 - src/ExtraUtils/spdlog/details/null_mutex.h | 45 - src/ExtraUtils/spdlog/details/os.h | 421 -- .../spdlog/details/pattern_formatter.h | 1336 ----- .../spdlog/details/periodic_worker.h | 71 - src/ExtraUtils/spdlog/details/registry.h | 285 - src/ExtraUtils/spdlog/details/thread_pool.h | 238 - src/ExtraUtils/spdlog/fmt/bin_to_hex.h | 172 - src/ExtraUtils/spdlog/fmt/bundled/LICENSE.rst | 23 - src/ExtraUtils/spdlog/fmt/bundled/chrono.h | 452 -- src/ExtraUtils/spdlog/fmt/bundled/color.h | 577 -- src/ExtraUtils/spdlog/fmt/bundled/core.h | 1502 ------ .../spdlog/fmt/bundled/format-inl.h | 972 ---- src/ExtraUtils/spdlog/fmt/bundled/format.h | 3555 ------------- src/ExtraUtils/spdlog/fmt/bundled/locale.h | 77 - src/ExtraUtils/spdlog/fmt/bundled/ostream.h | 153 - src/ExtraUtils/spdlog/fmt/bundled/posix.h | 324 -- src/ExtraUtils/spdlog/fmt/bundled/printf.h | 855 --- src/ExtraUtils/spdlog/fmt/bundled/ranges.h | 308 -- src/ExtraUtils/spdlog/fmt/bundled/time.h | 160 - src/ExtraUtils/spdlog/fmt/fmt.h | 25 - src/ExtraUtils/spdlog/fmt/ostr.h | 18 - src/ExtraUtils/spdlog/formatter.h | 20 - src/ExtraUtils/spdlog/logger.h | 183 - src/ExtraUtils/spdlog/sinks/android_sink.h | 121 - src/ExtraUtils/spdlog/sinks/ansicolor_sink.h | 161 - src/ExtraUtils/spdlog/sinks/base_sink.h | 69 - src/ExtraUtils/spdlog/sinks/basic_file_sink.h | 75 - src/ExtraUtils/spdlog/sinks/daily_file_sink.h | 141 - src/ExtraUtils/spdlog/sinks/dist_sink.h | 94 - src/ExtraUtils/spdlog/sinks/msvc_sink.h | 54 - src/ExtraUtils/spdlog/sinks/null_sink.h | 49 - src/ExtraUtils/spdlog/sinks/ostream_sink.h | 57 - .../spdlog/sinks/rotating_file_sink.h | 164 - src/ExtraUtils/spdlog/sinks/sink.h | 54 - .../spdlog/sinks/stdout_color_sinks.h | 56 - src/ExtraUtils/spdlog/sinks/stdout_sinks.h | 102 - src/ExtraUtils/spdlog/sinks/syslog_sink.h | 94 - src/ExtraUtils/spdlog/sinks/wincolor_sink.h | 143 - src/ExtraUtils/spdlog/spdlog.h | 366 -- src/ExtraUtils/spdlog/tweakme.h | 152 - src/ExtraUtils/spdlog/version.h | 12 - src/OpenDiablo2.Common/CMakeLists.txt | 31 - .../OpenDiablo2.Common/D2DataManager.h | 21 - .../OpenDiablo2.Common/D2EngineConfig.h | 16 - .../include/OpenDiablo2.Common/D2Palette.h | 55 - .../include/OpenDiablo2.Common/D2Point.h | 13 - .../OpenDiablo2.Common/D2ResourcePath.h | 467 -- .../include/OpenDiablo2.Common/D2Size.h | 13 - .../include/OpenDiablo2.Common/D2Sprite.h | 28 - src/OpenDiablo2.Common/src/D2DataManager.cpp | 61 - src/OpenDiablo2.Common/src/D2Sprite.cpp | 40 - src/OpenDiablo2.Game/CMakeLists.txt | 32 - .../include/OpenDiablo2.Game/D2Engine.h | 40 - .../OpenDiablo2.Game/Scenes/D2MainMenu.h | 19 - .../include/OpenDiablo2.Game/Scenes/D2Scene.h | 18 - src/OpenDiablo2.Game/src/D2Engine.cpp | 31 - .../src/Scenes/D2MainMenu.cpp | 15 - src/OpenDiablo2.Game/src/main.cpp | 48 - src/OpenDiablo2.SDL2/CMakeLists.txt | 35 - .../include/OpenDiablo2.System/D2Graphics.h | 49 - .../include/OpenDiablo2.System/D2Input.h | 20 - src/OpenDiablo2.SDL2/src/D2Graphics.cpp | 51 - src/OpenDiablo2.SDL2/src/D2Input.cpp | 25 - src/cmake/FindSDL2.cmake | 173 - src/cmake/FindSDL2_mixer.cmake | 100 - 97 files changed, 1572 insertions(+), 21378 deletions(-) delete mode 100644 .gitattributes delete mode 100644 .gitmodules create mode 100644 App/OpenDiablo2.go create mode 100644 OpenDiablo2/Common/SceneInterface.go create mode 100644 OpenDiablo2/CryptoBuff.go create mode 100644 OpenDiablo2/Engine.go create mode 100644 OpenDiablo2/MPQ.go create mode 100644 OpenDiablo2/MPQFont.go create mode 100644 OpenDiablo2/MPQStream.go create mode 100644 OpenDiablo2/Math.go create mode 100644 OpenDiablo2/Palette.go create mode 100644 OpenDiablo2/ResourcePaths/ResourcePaths.go create mode 100644 OpenDiablo2/SceneMainMenu.go create mode 100644 OpenDiablo2/SoundEntry.go create mode 100644 OpenDiablo2/Sprite.go create mode 100644 OpenDiablo2/StringUtils.go create mode 100644 OpenDiablo2/UILabel.go create mode 100644 config.json delete mode 100644 src/CMakeLists.txt delete mode 100644 src/ExtraUtils/CLI11/CLI11.hpp delete mode 100644 src/ExtraUtils/spdlog/async.h delete mode 100644 src/ExtraUtils/spdlog/async_logger.h delete mode 100644 src/ExtraUtils/spdlog/common.h delete mode 100644 src/ExtraUtils/spdlog/details/async_logger_impl.h delete mode 100644 src/ExtraUtils/spdlog/details/circular_q.h delete mode 100644 src/ExtraUtils/spdlog/details/console_globals.h delete mode 100644 src/ExtraUtils/spdlog/details/file_helper.h delete mode 100644 src/ExtraUtils/spdlog/details/fmt_helper.h delete mode 100644 src/ExtraUtils/spdlog/details/log_msg.h delete mode 100644 src/ExtraUtils/spdlog/details/logger_impl.h delete mode 100644 src/ExtraUtils/spdlog/details/mpmc_blocking_q.h delete mode 100644 src/ExtraUtils/spdlog/details/null_mutex.h delete mode 100644 src/ExtraUtils/spdlog/details/os.h delete mode 100644 src/ExtraUtils/spdlog/details/pattern_formatter.h delete mode 100644 src/ExtraUtils/spdlog/details/periodic_worker.h delete mode 100644 src/ExtraUtils/spdlog/details/registry.h delete mode 100644 src/ExtraUtils/spdlog/details/thread_pool.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bin_to_hex.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/LICENSE.rst delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/chrono.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/color.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/core.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/format-inl.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/format.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/locale.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/ostream.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/posix.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/printf.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/ranges.h delete mode 100644 src/ExtraUtils/spdlog/fmt/bundled/time.h delete mode 100644 src/ExtraUtils/spdlog/fmt/fmt.h delete mode 100644 src/ExtraUtils/spdlog/fmt/ostr.h delete mode 100644 src/ExtraUtils/spdlog/formatter.h delete mode 100644 src/ExtraUtils/spdlog/logger.h delete mode 100644 src/ExtraUtils/spdlog/sinks/android_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/ansicolor_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/base_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/basic_file_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/daily_file_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/dist_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/msvc_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/null_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/ostream_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/rotating_file_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/stdout_color_sinks.h delete mode 100644 src/ExtraUtils/spdlog/sinks/stdout_sinks.h delete mode 100644 src/ExtraUtils/spdlog/sinks/syslog_sink.h delete mode 100644 src/ExtraUtils/spdlog/sinks/wincolor_sink.h delete mode 100644 src/ExtraUtils/spdlog/spdlog.h delete mode 100644 src/ExtraUtils/spdlog/tweakme.h delete mode 100644 src/ExtraUtils/spdlog/version.h delete mode 100644 src/OpenDiablo2.Common/CMakeLists.txt delete mode 100644 src/OpenDiablo2.Common/include/OpenDiablo2.Common/D2DataManager.h delete mode 100644 src/OpenDiablo2.Common/include/OpenDiablo2.Common/D2EngineConfig.h delete mode 100644 src/OpenDiablo2.Common/include/OpenDiablo2.Common/D2Palette.h delete mode 100644 src/OpenDiablo2.Common/include/OpenDiablo2.Common/D2Point.h delete mode 100644 src/OpenDiablo2.Common/include/OpenDiablo2.Common/D2ResourcePath.h delete mode 100644 src/OpenDiablo2.Common/include/OpenDiablo2.Common/D2Size.h delete mode 100644 src/OpenDiablo2.Common/include/OpenDiablo2.Common/D2Sprite.h delete mode 100644 src/OpenDiablo2.Common/src/D2DataManager.cpp delete mode 100644 src/OpenDiablo2.Common/src/D2Sprite.cpp delete mode 100644 src/OpenDiablo2.Game/CMakeLists.txt delete mode 100644 src/OpenDiablo2.Game/include/OpenDiablo2.Game/D2Engine.h delete mode 100644 src/OpenDiablo2.Game/include/OpenDiablo2.Game/Scenes/D2MainMenu.h delete mode 100644 src/OpenDiablo2.Game/include/OpenDiablo2.Game/Scenes/D2Scene.h delete mode 100644 src/OpenDiablo2.Game/src/D2Engine.cpp delete mode 100644 src/OpenDiablo2.Game/src/Scenes/D2MainMenu.cpp delete mode 100644 src/OpenDiablo2.Game/src/main.cpp delete mode 100644 src/OpenDiablo2.SDL2/CMakeLists.txt delete mode 100644 src/OpenDiablo2.SDL2/include/OpenDiablo2.System/D2Graphics.h delete mode 100644 src/OpenDiablo2.SDL2/include/OpenDiablo2.System/D2Input.h delete mode 100644 src/OpenDiablo2.SDL2/src/D2Graphics.cpp delete mode 100644 src/OpenDiablo2.SDL2/src/D2Input.cpp delete mode 100644 src/cmake/FindSDL2.cmake delete mode 100644 src/cmake/FindSDL2_mixer.cmake diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 40f59fc3..00000000 --- a/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -# Auto detect text files and perform LF normalization -# http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ -* text=auto - -*.cs diff=csharp diff --git a/.gitignore b/.gitignore index 49fb29be..d0491558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -cmake-build-* -.idea -.vscode -build/ +__debug_bin +.vscode/*.* diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29b..00000000 diff --git a/App/OpenDiablo2.go b/App/OpenDiablo2.go new file mode 100644 index 00000000..aa2f96d7 --- /dev/null +++ b/App/OpenDiablo2.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + + "../OpenDiablo2" + "github.com/hajimehoshi/ebiten" +) + +var d2Engine *OpenDiablo2.Engine + +func main() { + log.Println("OpenDiablo2 - Open source Diablo 2 engine") + OpenDiablo2.InitializeCryptoBuffer() + d2Engine = OpenDiablo2.CreateEngine() + ebiten.SetCursorVisible(false) + ebiten.SetFullscreen(d2Engine.Settings.FullScreen) + ebiten.SetRunnableInBackground(d2Engine.Settings.RunInBackground) + ebiten.SetVsyncEnabled(d2Engine.Settings.VsyncEnabled) + ebiten.SetMaxTPS(d2Engine.Settings.TicksPerSecond) + if err := ebiten.Run(update, 800, 600, d2Engine.Settings.Scale, "OpenDiablo 2"); err != nil { + log.Fatal(err) + } +} + +func update(screen *ebiten.Image) error { + d2Engine.Update() + if ebiten.IsDrawingSkipped() { + return nil + } + d2Engine.Draw(screen) + return nil +} diff --git a/OpenDiablo2/Common/SceneInterface.go b/OpenDiablo2/Common/SceneInterface.go new file mode 100644 index 00000000..6bd0f8a1 --- /dev/null +++ b/OpenDiablo2/Common/SceneInterface.go @@ -0,0 +1,12 @@ +package Common + +import ( + "github.com/hajimehoshi/ebiten" +) + +type SceneInterface interface { + Load() + Unload() + Render(screen *ebiten.Image) + Update() +} diff --git a/OpenDiablo2/CryptoBuff.go b/OpenDiablo2/CryptoBuff.go new file mode 100644 index 00000000..31ac884a --- /dev/null +++ b/OpenDiablo2/CryptoBuff.go @@ -0,0 +1,20 @@ +package OpenDiablo2 + +// CryptoBuffer contains the crypto bytes for filename hashing +var CryptoBuffer [0x500]uint32 + +// InitializeCryptoBuffer initializes the crypto buffer +func InitializeCryptoBuffer() { + 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 + } + } +} diff --git a/OpenDiablo2/Engine.go b/OpenDiablo2/Engine.go new file mode 100644 index 00000000..779fd743 --- /dev/null +++ b/OpenDiablo2/Engine.go @@ -0,0 +1,212 @@ +package OpenDiablo2 + +import ( + "encoding/json" + "io/ioutil" + "log" + "path" + "strings" + "sync" + + "./Common" + "./ResourcePaths" + + "github.com/hajimehoshi/ebiten" +) + +// EngineConfig defines the configuration for the engine, loaded from config.json +type EngineConfig struct { + FullScreen bool + Scale float64 + RunInBackground bool + TicksPerSecond int + VsyncEnabled bool + MpqPath string + MpqLoadOrder []string +} + +// Engine is the core OpenDiablo2 engine +type Engine struct { + Settings EngineConfig // Engine configuration settings from json file + Files map[string]string // Map that defines which files are in which MPQs + Palettes map[string]Palette // Color palettes + SoundEntries map[string]SoundEntry // Sound configurations + CursorSprite Sprite // The sprite shown for cursors + LoadingSprite Sprite // The sprite shown when loading stuff + CursorX int // X position of the cursor + CursorY int // Y position of the cursor + LoadingProgress float64 // LoadingProcess is a range between 0.0 and 1.0. If set, loading screen displays. + CurrentScene Common.SceneInterface // The current scene being rendered + nextScene Common.SceneInterface // The next scene to be loaded at the end of the game loop + fontCache map[string]*MPQFont // The font cash +} + +// CreateEngine creates and instance of the OpenDiablo2 engine +func CreateEngine() *Engine { + result := &Engine{ + LoadingProgress: float64(0.0), + CurrentScene: nil, + nextScene: nil, + fontCache: make(map[string]*MPQFont), + } + result.loadConfigurationFile() + result.mapMpqFiles() + result.loadPalettes() + result.loadSoundEntries() + result.CursorSprite = result.LoadSprite(ResourcePaths.CursorDefault, result.Palettes["units"]) + result.LoadingSprite = result.LoadSprite(ResourcePaths.LoadingScreen, result.Palettes["loading"]) + loadingSpriteSizeX, loadingSpriteSizeY := result.LoadingSprite.GetSize() + result.LoadingSprite.MoveTo(int(400-(loadingSpriteSizeX/2)), int(300+(loadingSpriteSizeY/2))) + result.SetNextScene(CreateMainMenu(result)) + return result +} + +func (v *Engine) loadConfigurationFile() { + log.Println("loading configuration file") + configJSON, err := ioutil.ReadFile("config.json") + if err != nil { + panic(err) + } + var config EngineConfig + + json.Unmarshal(configJSON, &config) + v.Settings = config +} + +func (v *Engine) mapMpqFiles() { + log.Println("mapping mpq file structure") + v.Files = make(map[string]string) + lock := sync.RWMutex{} + for _, mpqFileName := range v.Settings.MpqLoadOrder { + mpqPath := path.Join(v.Settings.MpqPath, mpqFileName) + mpq, err := LoadMPQ(mpqPath) + if err != nil { + panic(err) + } + fileListText, err := mpq.ReadFile("(listfile)") + if err != nil { + panic(err) + } + fileList := strings.Split(string(fileListText), "\r\n") + for _, filePath := range fileList { + if _, exists := v.Files[strings.ToLower(filePath)]; exists { + lock.RUnlock() + continue + } + v.Files[`/`+strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)] = mpqPath + } + } +} + +// GetFile loads a file from the specified mpq and returns the data as a byte array +func (v *Engine) GetFile(fileName string) []byte { + // TODO: May want to cache some things if performance becomes an issue + mpqFile := v.Files[strings.ToLower(fileName)] + mpq, err := LoadMPQ(mpqFile) + if err != nil { + panic(err) + } + blockTableEntry, err := mpq.getFileBlockData(strings.ReplaceAll(fileName, `/`, `\`)[1:]) + if err != nil { + panic(err) + } + mpqStream := CreateMPQStream(mpq, blockTableEntry, fileName) + result := make([]byte, blockTableEntry.UncompressedFileSize) + mpqStream.Read(result, 0, blockTableEntry.UncompressedFileSize) + + return result +} + +// IsLoading returns true if the engine is currently in a loading state +func (v *Engine) IsLoading() bool { + return v.LoadingProgress < 1.0 +} + +func (v *Engine) loadPalettes() { + v.Palettes = make(map[string]Palette) + log.Println("loading palettes") + for file := range v.Files { + if strings.Index(file, "/data/global/palette/") != 0 || strings.Index(file, ".dat") != len(file)-4 { + continue + } + nameParts := strings.Split(file, `/`) + paletteName := nameParts[len(nameParts)-2] + palette := CreatePalette(paletteName, v.GetFile(file)) + v.Palettes[paletteName] = palette + } +} + +func (v *Engine) loadSoundEntries() { + log.Println("loading sound configurations") + v.SoundEntries = make(map[string]SoundEntry) + soundData := strings.Split(string(v.GetFile(ResourcePaths.SoundSettings)), "\r\n")[1:] + for _, line := range soundData { + if len(line) == 0 { + continue + } + soundEntry := CreateSoundEntry(line) + v.SoundEntries[soundEntry.Handle] = soundEntry + } +} + +// LoadSprite loads a sprite from the game's data files +func (v *Engine) LoadSprite(fileName string, palette Palette) Sprite { + data := v.GetFile(fileName) + sprite := CreateSprite(data, palette) + return sprite +} + +// updateScene handles the scene maintenance for the engine +func (v *Engine) updateScene() { + if v.nextScene == nil { + return + } + if v.CurrentScene != nil { + v.CurrentScene.Unload() + } + v.CurrentScene = v.nextScene + v.nextScene = nil + v.CurrentScene.Load() +} + +// Update updates the internal state of the engine +func (v *Engine) Update() { + v.updateScene() + if v.CurrentScene == nil { + panic("no scene loaded") + } + v.CurrentScene.Update() +} + +// Draw draws the game +func (v *Engine) Draw(screen *ebiten.Image) { + v.CursorX, v.CursorY = ebiten.CursorPosition() + if v.LoadingProgress < 1.0 { + v.LoadingSprite.Frame = uint8(Max(0, Min(uint32(len(v.LoadingSprite.Frames)-1), uint32(float64(len(v.LoadingSprite.Frames)-1)*v.LoadingProgress)))) + v.LoadingSprite.Draw(screen) + } else { + if v.CurrentScene == nil { + panic("no scene loaded") + } + v.CurrentScene.Render(screen) + } + + v.CursorSprite.MoveTo(v.CursorX, v.CursorY) + v.CursorSprite.Draw(screen) +} + +// SetNextScene tells the engine what scene to load on the next update cycle +func (v *Engine) SetNextScene(nextScene Common.SceneInterface) { + v.nextScene = nextScene +} + +// GetFont creates or loads an existing font +func (v *Engine) GetFont(font, palette string) *MPQFont { + cacheItem, exists := v.fontCache[font+"_"+palette] + if exists { + return cacheItem + } + newFont := CreateMPQFont(v, font, v.Palettes[palette]) + v.fontCache[font+"_"+palette] = newFont + return newFont +} diff --git a/OpenDiablo2/MPQ.go b/OpenDiablo2/MPQ.go new file mode 100644 index 00000000..d02813d6 --- /dev/null +++ b/OpenDiablo2/MPQ.go @@ -0,0 +1,256 @@ +package OpenDiablo2 + +import ( + "encoding/binary" + "errors" + "log" + "os" + "path" + "strings" +) + +// MPQ represents an MPQ archive +type MPQ struct { + File *os.File + HashTableEntries []MPQHashTableEntry + BlockTableEntries []MPQBlockTableEntry + Data MPQData +} + +// MPQData Represents a MPQ file +type MPQData struct { + Magic [4]byte + HeaderSize uint32 + ArchiveSize uint32 + FormatVersion uint16 + BlockSize uint16 + HashTableOffset uint32 + BlockTableOffset uint32 + HashTableEntries uint32 + BlockTableEntries uint32 +} + +// MPQHashTableEntry represents a hashed file entry in the MPQ file +type MPQHashTableEntry struct { // 16 bytes + NamePartA uint32 + NamePartB uint32 + Locale uint16 + Platform uint16 + BlockIndex uint32 +} + +// MPQFileFlag represents flags for a file record in the MPQ archive +type MPQFileFlag uint32 + +const ( + // MpqFileImplode - File is compressed using PKWARE Data compression library + MpqFileImplode MPQFileFlag = 0x00000100 + // MpqFileCompress - File is compressed using combination of compression methods + MpqFileCompress MPQFileFlag = 0x00000200 + // MpqFileEncrypted - The file is encrypted + MpqFileEncrypted MPQFileFlag = 0x00010000 + // MpqFileFixKey - The decryption key for the file is altered according to the position of the file in the archive + MpqFileFixKey MPQFileFlag = 0x00020000 + // MpqFilePatchFile - The file contains incremental patch for an existing file in base MPQ + MpqFilePatchFile MPQFileFlag = 0x00100000 + // MpqFileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit + MpqFileSingleUnit MPQFileFlag = 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 MPQFileFlag = 0x02000000 + // FileSEctorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded. + FileSEctorCrc MPQFileFlag = 0x04000000 + // MpqFileExists - Set if file exists, reset when the file was deleted + MpqFileExists MPQFileFlag = 0x80000000 +) + +// MPQBlockTableEntry represents an entry in the block table +type MPQBlockTableEntry struct { // 16 bytes + FilePosition uint32 + CompressedFileSize uint32 + UncompressedFileSize uint32 + Flags MPQFileFlag + // Local Stuff... + FileName string + EncryptionSeed uint32 +} + +// HasFlag returns true if the specified flag is present +func (v MPQBlockTableEntry) HasFlag(flag MPQFileFlag) bool { + return (v.Flags & flag) != 0 +} + +// LoadMPQ loads an MPQ file and returns a MPQ structure +func LoadMPQ(fileName string) (MPQ, error) { + result := MPQ{} + file, err := os.Open(fileName) + if err != nil { + return MPQ{}, err + } + result.File = file + err = result.readHeader() + if err != nil { + return MPQ{}, err + } + + return result, nil +} + +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") + } + v.loadHashTable() + v.loadBlockTable() + return nil +} + +func (v *MPQ) loadHashTable() { + v.File.Seek(int64(v.Data.HashTableOffset), 0) + hashData := make([]uint32, v.Data.HashTableEntries*4) + binary.Read(v.File, binary.LittleEndian, &hashData) + decrypt(hashData, hashString("(hash table)", 3)) + for i := uint32(0); i < v.Data.HashTableEntries; i++ { + v.HashTableEntries = append(v.HashTableEntries, MPQHashTableEntry{ + NamePartA: hashData[i*4], + NamePartB: hashData[(i*4)+1], + // TODO: Verify that we're grabbing the right high/lo word for the vars below + Locale: uint16(hashData[(i*4)+2] >> 16), + Platform: uint16(hashData[(i*4)+2] & 0xFFFF), + BlockIndex: hashData[(i*4)+3], + }) + } +} + +func (v *MPQ) loadBlockTable() { + v.File.Seek(int64(v.Data.BlockTableOffset), 0) + blockData := make([]uint32, v.Data.BlockTableEntries*4) + binary.Read(v.File, binary.LittleEndian, &blockData) + decrypt(blockData, hashString("(block table)", 3)) + for i := uint32(0); i < v.Data.BlockTableEntries; i++ { + v.BlockTableEntries = append(v.BlockTableEntries, MPQBlockTableEntry{ + FilePosition: blockData[(i * 4)], + CompressedFileSize: blockData[(i*4)+1], + UncompressedFileSize: blockData[(i*4)+2], + Flags: MPQFileFlag(blockData[(i*4)+3]), + }) + } +} + +func decrypt(data []uint32, seed uint32) { + seed2 := uint32(0xeeeeeeee) + + for i := 0; i < len(data); i++ { + seed2 += CryptoBuffer[0x400+(seed&0xff)] + result := data[i] + result ^= seed + seed2 + + seed = ((^seed << 21) + 0x11111111) | (seed >> 11) + seed2 = result + seed2 + (seed2 << 5) + 3 + data[i] = result + } +} + +func decryptBytes(data []byte, seed uint32) { + seed2 := uint32(0xEEEEEEEE) + for i := 0; i < len(data)-3; i += 4 { + seed2 += CryptoBuffer[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) + } +} + +func hashString(key string, hashType uint32) uint32 { + + seed1 := uint32(0x7FED7FED) + seed2 := uint32(0xEEEEEEEE) + + /* prepare seeds. */ + for _, char := range strings.ToUpper(key) { + seed1 = CryptoBuffer[(hashType*0x100)+uint32(char)] ^ (seed1 + seed2) + seed2 = uint32(char) + seed1 + seed2 + (seed2 << 5) + 3 + } + return seed1 +} + +func (v MPQ) getFileHashEntry(fileName string) (MPQHashTableEntry, error) { + hashA := hashString(fileName, 1) + hashB := hashString(fileName, 2) + + for idx, hashEntry := range v.HashTableEntries { + if hashEntry.NamePartA != hashA || hashEntry.NamePartB != hashB { + continue + } + + return v.HashTableEntries[idx], nil + } + return MPQHashTableEntry{}, errors.New("file not found") +} + +func (v MPQ) getFileBlockData(fileName string) (MPQBlockTableEntry, error) { + fileEntry, err := v.getFileHashEntry(fileName) + if err != nil { + return MPQBlockTableEntry{}, err + } + return v.BlockTableEntries[fileEntry.BlockIndex], nil +} + +// Close closses the MPQ file +func (v *MPQ) Close() { + v.File.Close() +} + +// 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 nil, err + } + fileBlockData.FileName = strings.ToLower(fileName) + fileBlockData.calculateEncryptionSeed() + mpqStream := CreateMPQStream(v, fileBlockData, fileName) + buffer := make([]byte, fileBlockData.UncompressedFileSize) + mpqStream.Read(buffer, 0, fileBlockData.UncompressedFileSize) + return buffer, 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 *MPQBlockTableEntry) calculateEncryptionSeed() { + fileName := path.Base(v.FileName) + v.EncryptionSeed = hashString(fileName, 3) + if !v.HasFlag(MpqFileFixKey) { + 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 + } + log.Printf("File Contents:\n%s", strings.TrimRight(string(data), "\x00")) + data = nil + return []string{""}, nil +} diff --git a/OpenDiablo2/MPQFont.go b/OpenDiablo2/MPQFont.go new file mode 100644 index 00000000..731078d5 --- /dev/null +++ b/OpenDiablo2/MPQFont.go @@ -0,0 +1,34 @@ +package OpenDiablo2 + +type MPQFontSize struct { + Width uint8 + Height uint8 +} + +type MPQFont struct { + Engine *Engine + FontSprite Sprite + Metrics map[uint8]MPQFontSize +} + +// CreateMPQFont creates an instance of a MPQ Font +func CreateMPQFont(engine *Engine, font string, palette Palette) *MPQFont { + result := &MPQFont{ + Engine: engine, + Metrics: make(map[uint8]MPQFontSize), + } + result.FontSprite = result.Engine.LoadSprite(font+".dc6", palette) + woo := "Woo!\x01" + fontData := result.Engine.GetFile(font + ".tbl") + if string(fontData[0:5]) != woo { + panic("No woo :(") + } + for i := 12; i < len(fontData); i += 14 { + fontSize := MPQFontSize{ + Width: fontData[i+3], + Height: fontData[i+4], + } + result.Metrics[fontData[i+8]] = fontSize + } + return result +} diff --git a/OpenDiablo2/MPQStream.go b/OpenDiablo2/MPQStream.go new file mode 100644 index 00000000..40812241 --- /dev/null +++ b/OpenDiablo2/MPQStream.go @@ -0,0 +1,229 @@ +package OpenDiablo2 + +import ( + "bytes" + "compress/zlib" + "encoding/binary" + "fmt" + + "github.com/JoshVarga/blast" +) + +// MPQStream represents a stream of data in an MPQ archive +type MPQStream struct { + MPQData MPQ + BlockTableEntry MPQBlockTableEntry + FileName string + EncryptionSeed uint32 + BlockPositions []uint32 + CurrentPosition uint32 + CurrentData []byte + CurrentBlockIndex uint32 + BlockSize uint32 +} + +// CreateMPQStream creates an MPQ stream +func CreateMPQStream(mpq MPQ, blockTableEntry MPQBlockTableEntry, fileName string) MPQStream { + result := MPQStream{ + MPQData: mpq, + BlockTableEntry: blockTableEntry, + CurrentBlockIndex: 0xFFFFFFFF, + } + result.EncryptionSeed = hashString(fileName, 3) + if result.BlockTableEntry.HasFlag(MpqFileFixKey) { + result.EncryptionSeed = (result.EncryptionSeed + result.BlockTableEntry.FilePosition) ^ result.BlockTableEntry.UncompressedFileSize + } + result.BlockSize = 0x200 << result.MPQData.Data.BlockSize + + if (result.BlockTableEntry.HasFlag(MpqFileCompress) || result.BlockTableEntry.HasFlag(MpqFileImplode)) && !result.BlockTableEntry.HasFlag(MpqFileSingleUnit) { + result.loadBlockOffsets() + } + return result +} + +func (v *MPQStream) loadBlockOffsets() { + blockPositionCount := ((v.BlockTableEntry.UncompressedFileSize + v.BlockSize - 1) / v.BlockSize) + 1 + v.BlockPositions = make([]uint32, blockPositionCount) + v.MPQData.File.Seek(int64(v.BlockTableEntry.FilePosition), 0) + binary.Read(v.MPQData.File, binary.LittleEndian, &v.BlockPositions) + blockPosSize := blockPositionCount << 2 + if v.BlockTableEntry.HasFlag(MpqFileEncrypted) { + decrypt(v.BlockPositions, v.EncryptionSeed-1) + if v.BlockPositions[0] != blockPosSize { + panic("Decryption of MPQ failed!") + } + if v.BlockPositions[1] > v.BlockSize+blockPosSize { + panic("Decryption of MPQ failed!") + } + } +} + +func (v *MPQStream) Read(buffer []byte, offset, count uint32) uint32 { + if v.BlockTableEntry.HasFlag(MpqFileSingleUnit) { + return v.readInternalSingleUnit(buffer, offset, count) + } + toRead := count + readTotal := uint32(0) + for toRead > 0 { + read := v.readInternal(buffer, offset, count) + if read == 0 { + break + } + readTotal += read + offset += read + toRead -= read + } + return readTotal +} + +func (v *MPQStream) readInternalSingleUnit(buffer []byte, offset, count uint32) uint32 { + if len(v.CurrentData) == 0 { + v.loadSingleUnit() + } + + bytesToCopy := Min(uint32(len(v.CurrentData))-v.CurrentPosition, count) + copy(buffer[offset:offset+bytesToCopy], v.CurrentData[v.CurrentPosition:v.CurrentPosition+bytesToCopy]) + v.CurrentPosition += bytesToCopy + return bytesToCopy +} + +func (v *MPQStream) readInternal(buffer []byte, offset, count uint32) uint32 { + v.bufferData() + localPosition := v.CurrentPosition % v.BlockSize + bytesToCopy := Min(uint32(len(v.CurrentData))-localPosition, count) + if bytesToCopy <= 0 { + return 0 + } + copy(buffer[offset:offset+bytesToCopy], v.CurrentData[localPosition:localPosition+bytesToCopy]) + v.CurrentPosition += bytesToCopy + return bytesToCopy +} + +func (v *MPQStream) bufferData() { + requiredBlock := uint32(v.CurrentPosition / v.BlockSize) + if requiredBlock == v.CurrentBlockIndex { + return + } + expectedLength := Min(v.BlockTableEntry.UncompressedFileSize-(requiredBlock*v.BlockSize), v.BlockSize) + v.CurrentData = v.loadBlock(requiredBlock, expectedLength) + v.CurrentBlockIndex = requiredBlock +} + +func (v *MPQStream) loadSingleUnit() { + fileData := make([]byte, v.BlockSize) + v.MPQData.File.Seek(int64(v.MPQData.Data.HeaderSize), 0) + binary.Read(v.MPQData.File, binary.LittleEndian, &fileData) + if v.BlockSize == v.BlockTableEntry.UncompressedFileSize { + v.CurrentData = fileData + return + } + v.CurrentData = decompressMulti(fileData, v.BlockTableEntry.UncompressedFileSize) +} + +func (v *MPQStream) loadBlock(blockIndex, expectedLength uint32) []byte { + var ( + offset uint32 + toRead uint32 + ) + if v.BlockTableEntry.HasFlag(MpqFileCompress) || v.BlockTableEntry.HasFlag(MpqFileImplode) { + offset = v.BlockPositions[blockIndex] + toRead = v.BlockPositions[blockIndex+1] - offset + } else { + offset = blockIndex * v.BlockSize + toRead = expectedLength + } + offset += v.BlockTableEntry.FilePosition + data := make([]byte, toRead) + v.MPQData.File.Seek(int64(offset), 0) + binary.Read(v.MPQData.File, binary.LittleEndian, &data) + if v.BlockTableEntry.HasFlag(MpqFileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 { + if v.EncryptionSeed == 0 { + panic("Unable to determine encryption key") + } + + decryptBytes(data, blockIndex+v.EncryptionSeed) + } + if v.BlockTableEntry.HasFlag(MpqFileCompress) && (toRead != expectedLength) { + if !v.BlockTableEntry.HasFlag(MpqFileSingleUnit) { + data = decompressMulti(data, expectedLength) + } else { + data = pkDecompress(data) + } + } + if v.BlockTableEntry.HasFlag(MpqFileImplode) && (toRead != expectedLength) { + data = pkDecompress(data) + } + + return data +} + +func decompressMulti(data []byte, expectedLength uint32) []byte { + copmressionType := data[0] + switch copmressionType { + case 1: // Huffman + panic("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") + case 0x80: // IMA ADPCM Stereo + //return MpqWavCompression.Decompress(sinput, 2); + panic("ima adpcm sterio decompression not supported") + case 0x40: // IMA ADPCM Mono + //return MpqWavCompression.Decompress(sinput, 1) + panic("mpq wav decompression not supported") + case 0x12: + panic("lzma decompression not supported") + // Combos + case 0x22: + // TODO: sparse then zlib + panic("sparse decompression + deflate decompression not supported") + case 0x30: + // TODO: sparse then bzip2 + panic("sparse decompression + bzip2 decompression not supported") + case 0x41: + //sinput = MpqHuffman.Decompress(sinput); + //return MpqWavCompression.Decompress(sinput, 1); + panic("mpqhuffman decompression not supported") + case 0x48: + //byte[] result = PKDecompress(sinput, outputLength); + //return MpqWavCompression.Decompress(new MemoryStream(result), 1); + panic("pk + mpqwav decompression not supported") + case 0x81: + //sinput = MpqHuffman.Decompress(sinput); + //return MpqWavCompression.Decompress(sinput, 2); + panic("huff + mpqwav decompression not supported") + 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", copmressionType)) + } +} + +func deflate(data []byte) []byte { + b := bytes.NewReader(data) + r, err := zlib.NewReader(b) + defer r.Close() + if err != nil { + panic(err) + } + buffer := new(bytes.Buffer) + buffer.ReadFrom(r) + return buffer.Bytes() +} + +func pkDecompress(data []byte) []byte { + b := bytes.NewReader(data) + r, err := blast.NewReader(b) + defer r.Close() + if err != nil { + panic(err) + } + buffer := new(bytes.Buffer) + buffer.ReadFrom(r) + return buffer.Bytes() +} diff --git a/OpenDiablo2/Math.go b/OpenDiablo2/Math.go new file mode 100644 index 00000000..7f2d03c1 --- /dev/null +++ b/OpenDiablo2/Math.go @@ -0,0 +1,31 @@ +package OpenDiablo2 + +// Min returns the lower of two values +func Min(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +// Max returns the higher of two values +func Max(a, b uint32) uint32 { + if a > b { + return a + } + return b +} + +// Max returns the higher of two values +func MaxInt32(a, b int32) int32 { + if a > b { + return a + } + return b +} + +// BytesToInt32 converts 4 bytes to int32 +func BytesToInt32(b []byte) int32 { + // equivalnt of return int32(binary.LittleEndian.Uint32(b)) + return int32(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24) +} diff --git a/OpenDiablo2/Palette.go b/OpenDiablo2/Palette.go new file mode 100644 index 00000000..a77b544c --- /dev/null +++ b/OpenDiablo2/Palette.go @@ -0,0 +1,26 @@ +package OpenDiablo2 + +type PaletteRGB struct { + R, G, B uint8 +} + +// Palette represents a palette +type Palette struct { + Name string + Colors [256]PaletteRGB +} + +// CreatePalette creates a palette +func CreatePalette(name string, data []byte) Palette { + result := Palette{Name: name} + + for i := 0; i <= 255; i++ { + result.Colors[i] = PaletteRGB{ + B: data[i*3], + G: data[(i*3)+1], + R: data[(i*3)+2], + } + } + + return result +} diff --git a/OpenDiablo2/ResourcePaths/ResourcePaths.go b/OpenDiablo2/ResourcePaths/ResourcePaths.go new file mode 100644 index 00000000..34da9c5b --- /dev/null +++ b/OpenDiablo2/ResourcePaths/ResourcePaths.go @@ -0,0 +1,247 @@ +package ResourcePaths + +const ( + // --- Screens --- + + LoadingScreen = "/data/global/ui/Loading/loadingscreen.dc6" + + // --- Main Menu --- + + TrademarkScreen = "/data/global/ui/FrontEnd/trademarkscreenEXP.dc6" + GameSelectScreen = "/data/global/ui/FrontEnd/gameselectscreenEXP.dc6" + Diablo2LogoFireLeft = "/data/global/ui/FrontEnd/D2logoFireLeft.DC6" + Diablo2LogoFireRight = "/data/global/ui/FrontEnd/D2logoFireRight.DC6" + Diablo2LogoBlackLeft = "/data/global/ui/FrontEnd/D2logoBlackLeft.DC6" + Diablo2LogoBlackRight = "/data/global/ui/FrontEnd/D2logoBlackRight.DC6" + + // --- Credits --- + + CreditsBackground = "/data/global/ui/CharSelect/creditsbckgexpand.dc6" + CreditsText = "/data/local/ui/eng/ExpansionCredits.txt" + + // --- Character Select Screen --- + + CharacterSelectBackground = "/data/global/ui/FrontEnd/charactercreationscreenEXP.dc6" + CharacterSelectCampfire = "/data/global/ui/FrontEnd/fire.DC6" + + CharacterSelectBarbarianUnselected = "/data/global/ui/FrontEnd/barbarian/banu1.DC6" + CharacterSelectBarbarianUnselectedH = "/data/global/ui/FrontEnd/barbarian/banu2.DC6" + CharacterSelectBarbarianSelected = "/data/global/ui/FrontEnd/barbarian/banu3.DC6" + CharacterSelectBarbarianForwardWalk = "/data/global/ui/FrontEnd/barbarian/bafw.DC6" + CharacterSelectBarbarianForwardWalkOverlay = "/data/global/ui/FrontEnd/barbarian/BAFWs.DC6" + CharacterSelectBarbarianBackWalk = "/data/global/ui/FrontEnd/barbarian/babw.DC6" + + CharacterSelecSorceressUnselected = "/data/global/ui/FrontEnd/sorceress/SONU1.DC6" + CharacterSelecSorceressUnselectedH = "/data/global/ui/FrontEnd/sorceress/SONU2.DC6" + CharacterSelecSorceressSelected = "/data/global/ui/FrontEnd/sorceress/SONU3.DC6" + CharacterSelecSorceressSelectedOverlay = "/data/global/ui/FrontEnd/sorceress/SONU3s.DC6" + CharacterSelecSorceressForwardWalk = "/data/global/ui/FrontEnd/sorceress/SOFW.DC6" + CharacterSelecSorceressForwardWalkOverlay = "/data/global/ui/FrontEnd/sorceress/SOFWs.DC6" + CharacterSelecSorceressBackWalk = "/data/global/ui/FrontEnd/sorceress/SOBW.DC6" + CharacterSelecSorceressBackWalkOverlay = "/data/global/ui/FrontEnd/sorceress/SOBWs.DC6" + + CharacterSelectNecromancerUnselected = "/data/global/ui/FrontEnd/necromancer/NENU1.DC6" + CharacterSelectNecromancerUnselectedH = "/data/global/ui/FrontEnd/necromancer/NENU2.DC6" + CharacterSelecNecromancerSelected = "/data/global/ui/FrontEnd/necromancer/NENU3.DC6" + CharacterSelecNecromancerSelectedOverlay = "/data/global/ui/FrontEnd/necromancer/NENU3s.DC6" + CharacterSelecNecromancerForwardWalk = "/data/global/ui/FrontEnd/necromancer/NEFW.DC6" + CharacterSelecNecromancerForwardWalkOverlay = "/data/global/ui/FrontEnd/necromancer/NEFWs.DC6" + CharacterSelecNecromancerBackWalk = "/data/global/ui/FrontEnd/necromancer/NEBW.DC6" + CharacterSelecNecromancerBackWalkOverlay = "/data/global/ui/FrontEnd/necromancer/NEBWs.DC6" + + CharacterSelectPaladinUnselected = "/data/global/ui/FrontEnd/paladin/PANU1.DC6" + CharacterSelectPaladinUnselectedH = "/data/global/ui/FrontEnd/paladin/PANU2.DC6" + CharacterSelecPaladinSelected = "/data/global/ui/FrontEnd/paladin/PANU3.DC6" + CharacterSelecPaladinForwardWalk = "/data/global/ui/FrontEnd/paladin/PAFW.DC6" + CharacterSelecPaladinForwardWalkOverlay = "/data/global/ui/FrontEnd/paladin/PAFWs.DC6" + CharacterSelecPaladinBackWalk = "/data/global/ui/FrontEnd/paladin/PABW.DC6" + + CharacterSelectAmazonUnselected = "/data/global/ui/FrontEnd/amazon/AMNU1.DC6" + CharacterSelectAmazonUnselectedH = "/data/global/ui/FrontEnd/amazon/AMNU2.DC6" + CharacterSelecAmazonSelected = "/data/global/ui/FrontEnd/amazon/AMNU3.DC6" + CharacterSelecAmazonForwardWalk = "/data/global/ui/FrontEnd/amazon/AMFW.DC6" + CharacterSelecAmazonForwardWalkOverlay = "/data/global/ui/FrontEnd/amazon/AMFWs.DC6" + CharacterSelecAmazonBackWalk = "/data/global/ui/FrontEnd/amazon/AMBW.DC6" + + CharacterSelectAssassinUnselected = "/data/global/ui/FrontEnd/assassin/ASNU1.DC6" + CharacterSelectAssassinUnselectedH = "/data/global/ui/FrontEnd/assassin/ASNU2.DC6" + CharacterSelectAssassinSelected = "/data/global/ui/FrontEnd/assassin/ASNU3.DC6" + CharacterSelectAssassinForwardWalk = "/data/global/ui/FrontEnd/assassin/ASFW.DC6" + CharacterSelectAssassinBackWalk = "/data/global/ui/FrontEnd/assassin/ASBW.DC6" + + CharacterSelectDruidUnselected = "/data/global/ui/FrontEnd/druid/DZNU1.dc6" + CharacterSelectDruidUnselectedH = "/data/global/ui/FrontEnd/druid/DZNU2.dc6" + CharacterSelectDruidSelected = "/data/global/ui/FrontEnd/druid/DZNU3.DC6" + CharacterSelectDruidForwardWalk = "/data/global/ui/FrontEnd/druid/DZFW.DC6" + CharacterSelectDruidBackWalk = "/data/global/ui/FrontEnd/druid/DZBW.DC6" + + // -- Character Selection + + CharacterSelectionBackground = "/data/global/ui/CharSelect/characterselectscreenEXP.dc6" + + // --- Game --- + + GamePanels = "/data/global/ui/PANEL/800ctrlpnl7.dc6" + GameGlobeOverlap = "/data/global/ui/PANEL/overlap.DC6" + HealthMana = "/data/global/ui/PANEL/hlthmana.DC6" + GameSmallMenuButton = "/data/global/ui/PANEL/menubutton.DC6" // TODO: Used for inventory popout + SkillIcon = "/data/global/ui/PANEL/Skillicon.DC6" // TODO: Used for skill icon button + AddSkillButton = "/data/global/ui/PANEL/level.DC6" + + // --- Mouse Pointers --- + + CursorDefault = "/data/global/ui/CURSOR/ohand.DC6" + + // --- Fonts --- + + Font6 = "/data/local/font/latin/font6" + Font8 = "/data/local/font/latin/font8" + Font16 = "/data/local/font/latin/font16" + Font24 = "/data/local/font/latin/font24" + Font30 = "/data/local/font/latin/font30" + FontFormal12 = "/data/local/font/latin/fontformal12" + FontFormal11 = "/data/local/font/latin/fontformal11" + FontFormal10 = "/data/local/font/latin/fontformal10" + FontExocet10 = "/data/local/font/latin/fontexocet10" + FontExocet8 = "/data/local/font/latin/fontexocet8" + + // --- UI --- + + WideButtonBlank = "/data/global/ui/FrontEnd/WideButtonBlank.dc6" + MediumButtonBlank = "/data/global/ui/FrontEnd/MediumButtonBlank.dc6" + CancelButton = "/data/global/ui/FrontEnd/CancelButtonBlank.dc6" + NarrowButtonBlank = "/data/global/ui/FrontEnd/NarrowButtonBlank.dc6" + ShortButtonBlank = "/data/global/ui/CharSelect/ShortButtonBlank.dc6" + TextBox2 = "/data/global/ui/FrontEnd/textbox2.dc6" + TallButtonBlank = "/data/global/ui/CharSelect/TallButtonBlank.dc6" + + // --- GAME UI --- + + 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" + + GenericSkills = "/data/global/ui/SPELLS/Skillicon.DC6" + AmazonSkills = "/data/global/ui/SPELLS/AmSkillicon.DC6" + BarbarianSkills = "/data/global/ui/SPELLS/BaSkillicon.DC6" + DruidSkills = "/data/global/ui/SPELLS/DrSkillicon.DC6" + AssassinSkills = "/data/global/ui/SPELLS/AsSkillicon.DC6" + NecromancerSkills = "/data/global/ui/SPELLS/NeSkillicon.DC6" + PaladinSkills = "/data/global/ui/SPELLS/PaSkillicon.DC6" + SorcererSkills = "/data/global/ui/SPELLS/SoSkillicon.DC6" + + RunButton = "/data/global/ui/PANEL/runbutton.dc6" + MenuButton = "/data/global/ui/PANEL/menubutton.DC6" + GoldCoinButton = "/data/global/ui/panel/goldcoinbtn.dc6" + SquareButton = "/data/global/ui/panel/buysellbtn.dc6" + + ArmorPlaceholder = "/data/global/ui/PANEL/inv_armor.DC6" + BeltPlaceholder = "/data/global/ui/PANEL/inv_belt.DC6" + BootsPlaceholder = "/data/global/ui/PANEL/inv_boots.DC6" + HelmGlovePlaceholder = "/data/global/ui/PANEL/inv_helm_glove.DC6" + RingAmuletPlaceholder = "/data/global/ui/PANEL/inv_ring_amulet.DC6" + WeaponsPlaceholder = "/data/global/ui/PANEL/inv_weapons.DC6" + + // --- Data --- + + EnglishTable = "/data/local/lng/eng/English.txt" + ExpansionStringTable = "/data/local/lng/eng/expansionstring.tbl" + LevelPreset = "/data/global/excel/LvlPrest.txt" + LevelType = "/data/global/excel/LvlTypes.txt" + LevelDetails = "/data/global/excel/Levels.txt" + ObjectDetails = "/data/global/excel/Objects.txt" + SoundSettings = "/data/global/excel/Sounds.txt" + + // --- Animations --- + + ObjectData = "/data/global/objects" + AnimationData = "/data/global/animdata.d2" + PlayerAnimationBase = "/data/global/CHARS" + + // --- Inventory Data --- + + Weapons = "/data/global/excel/weapons.txt" + Armor = "/data/global/excel/armor.txt" + Misc = "/data/global/excel/misc.txt" + + // --- Character Data --- + + Experience = "/data/global/excel/experience.txt" + CharStats = "/data/global/excel/charstats.txt" + + // --- Music --- + + BGMTitle = "/data/global/music/introedit.wav" + BGMOptions = "/data/global/music/Common/options.wav" + BGMAct1AndarielAction = "/data/global/music/Act1/andarielaction.wav" + BGMAct1BloodRavenResolution = "/data/global/music/Act1/bloodravenresolution.wav" + BGMAct1Caves = "/data/global/music/Act1/caves.wav" + BGMAct1Crypt = "/data/global/music/Act1/crypt.wav" + BGMAct1DenOfEvilAction = "/data/global/music/Act1/denofevilaction.wav" + BGMAct1Monastery = "/data/global/music/Act1/monastery.wav" + BGMAct1Town1 = "/data/global/music/Act1/town1.wav" + BGMAct1Tristram = "/data/global/music/Act1/tristram.wav" + BGMAct1Wild = "/data/global/music/Act1/wild.wav" + BGMAct2Desert = "/data/global/music/Act2/desert.wav" + BGMAct2Harem = "/data/global/music/Act2/harem.wav" + BGMAct2HoradricAction = "/data/global/music/Act2/horadricaction.wav" + BGMAct2Lair = "/data/global/music/Act2/lair.wav" + BGMAct2RadamentResolution = "/data/global/music/Act2/radamentresolution.wav" + BGMAct2Sanctuary = "/data/global/music/Act2/sanctuary.wav" + BGMAct2Sewer = "/data/global/music/Act2/sewer.wav" + BGMAct2TaintedSunAction = "/data/global/music/Act2/taintedsunaction.wav" + BGMAct2Tombs = "/data/global/music/Act2/tombs.wav" + BGMAct2Town2 = "/data/global/music/Act2/town2.wav" + BGMAct2Valley = "/data/global/music/Act2/valley.wav" + BGMAct3Jungle = "/data/global/music/Act3/jungle.wav" + BGMAct3Kurast = "/data/global/music/Act3/kurast.wav" + BGMAct3KurastSewer = "/data/global/music/Act3/kurastsewer.wav" + BGMAct3MefDeathAction = "/data/global/music/Act3/mefdeathaction.wav" + BGMAct3OrbAction = "/data/global/music/Act3/orbaction.wav" + BGMAct3Spider = "/data/global/music/Act3/spider.wav" + BGMAct3Town3 = "/data/global/music/Act3/town3.wav" + BGMAct4Diablo = "/data/global/music/Act4/diablo.wav" + BGMAct4DiabloAction = "/data/global/music/Act4/diabloaction.wav" + BGMAct4ForgeAction = "/data/global/music/Act4/forgeaction.wav" + BGMAct4IzualAction = "/data/global/music/Act4/izualaction.wav" + BGMAct4Mesa = "/data/global/music/Act4/mesa.wav" + BGMAct4Town4 = "/data/global/music/Act4/town4.wav" + BGMAct5Baal = "/data/global/music/Act5/baal.wav" + BGMAct5XTown = "/data/global/music/Act5/xtown.wav" + + // --- Sound Effects --- + + SFXButtonClick = "/data/global/sfx/Cursor/button.wav" + SFXAmazonDeselect = "/data/global/sfx/Cursor/intro/amazon deselect.wav" + SFXAmazonSelect = "/data/global/sfx/Cursor/intro/amazon select.wav" + SFXAssassinDeselect = "/data/global/sfx/Cursor/intro/assassin deselect.wav" + SFXAssassinSelect = "/data/global/sfx/Cursor/intro/assassin select.wav" + SFXBarbarianDeselect = "/data/global/sfx/Cursor/intro/barbarian deselect.wav" + SFXBarbarianSelect = "/data/global/sfx/Cursor/intro/barbarian select.wav" + SFXDruidDeselect = "/data/global/sfx/Cursor/intro/druid deselect.wav" + SFXDruidSelect = "/data/global/sfx/Cursor/intro/druid select.wav" + SFXNecromancerDeselect = "/data/global/sfx/Cursor/intro/necromancer deselect.wav" + SFXNecromancerSelect = "/data/global/sfx/Cursor/intro/necromancer select.wav" + SFXPaladinDeselect = "/data/global/sfx/Cursor/intro/paladin deselect.wav" + SFXPaladinSelect = "/data/global/sfx/Cursor/intro/paladin select.wav" + SFXSorceressDeselect = "/data/global/sfx/Cursor/intro/sorceress deselect.wav" + SFXSorceressSelect = "/data/global/sfx/Cursor/intro/sorceress select.wav" + + // --- Enemy Data --- + + MonStats = "/data//global//excel//monstats.txt" + + // --- Skill Data --- + + Missiles = "/data//global//excel//missiles.txt" +) diff --git a/OpenDiablo2/SceneMainMenu.go b/OpenDiablo2/SceneMainMenu.go new file mode 100644 index 00000000..aa3ce3e9 --- /dev/null +++ b/OpenDiablo2/SceneMainMenu.go @@ -0,0 +1,86 @@ +package OpenDiablo2 + +import ( + "./ResourcePaths" + "github.com/hajimehoshi/ebiten" +) + +type MainMenu struct { + Engine *Engine + TrademarkBackground Sprite + Background Sprite + DiabloLogoLeft Sprite + DiabloLogoRight Sprite + DiabloLogoLeftBack Sprite + DiabloLogoRightBack Sprite + CopyrightLabel *UILabel + ShowTrademarkScreen bool +} + +func CreateMainMenu(engine *Engine) *MainMenu { + result := &MainMenu{ + Engine: engine, + ShowTrademarkScreen: true, + } + + return result +} + +func (v *MainMenu) Load() { + go func() { + loadStep := 1.0 / 7.0 + v.Engine.LoadingProgress = 0 + v.CopyrightLabel = CreateUILabel(v.Engine, ResourcePaths.FontFormal11, "static") + v.CopyrightLabel.SetText("Hello, world!") + v.CopyrightLabel.MoveTo(0, 0) + v.Engine.LoadingProgress += loadStep + v.Background = v.Engine.LoadSprite(ResourcePaths.GameSelectScreen, v.Engine.Palettes["sky"]) + v.Background.MoveTo(0, 0) + v.Engine.LoadingProgress += loadStep + v.TrademarkBackground = v.Engine.LoadSprite(ResourcePaths.TrademarkScreen, v.Engine.Palettes["sky"]) + v.TrademarkBackground.MoveTo(0, 0) + v.Engine.LoadingProgress += loadStep + v.DiabloLogoLeft = v.Engine.LoadSprite(ResourcePaths.Diablo2LogoFireLeft, v.Engine.Palettes["units"]) + v.DiabloLogoLeft.Blend = true + v.DiabloLogoLeft.Animate = true + v.DiabloLogoLeft.MoveTo(400, 120) + v.Engine.LoadingProgress += loadStep + v.DiabloLogoRight = v.Engine.LoadSprite(ResourcePaths.Diablo2LogoFireRight, v.Engine.Palettes["units"]) + v.DiabloLogoRight.Blend = true + v.DiabloLogoRight.Animate = true + v.DiabloLogoRight.MoveTo(400, 120) + v.Engine.LoadingProgress += loadStep + v.DiabloLogoLeftBack = v.Engine.LoadSprite(ResourcePaths.Diablo2LogoBlackLeft, v.Engine.Palettes["units"]) + v.DiabloLogoLeftBack.MoveTo(400, 120) + v.Engine.LoadingProgress += loadStep + v.DiabloLogoRightBack = v.Engine.LoadSprite(ResourcePaths.Diablo2LogoBlackRight, v.Engine.Palettes["units"]) + v.DiabloLogoRightBack.MoveTo(400, 120) + v.Engine.LoadingProgress = 1.0 + }() +} + +func (v *MainMenu) Unload() { + +} + +func (v *MainMenu) Render(screen *ebiten.Image) { + if v.ShowTrademarkScreen { + v.TrademarkBackground.DrawSegments(screen, 4, 3, 0) + } else { + v.Background.DrawSegments(screen, 4, 3, 0) + } + v.DiabloLogoLeftBack.Draw(screen) + v.DiabloLogoRightBack.Draw(screen) + v.DiabloLogoLeft.Draw(screen) + v.DiabloLogoRight.Draw(screen) + + if v.ShowTrademarkScreen { + v.CopyrightLabel.Draw(screen) + } else { + + } +} + +func (v *MainMenu) Update() { + +} diff --git a/OpenDiablo2/SoundEntry.go b/OpenDiablo2/SoundEntry.go new file mode 100644 index 00000000..680dd9b3 --- /dev/null +++ b/OpenDiablo2/SoundEntry.go @@ -0,0 +1,67 @@ +package OpenDiablo2 + +import ( + "strings" +) + +// SoundEntry represents a sound entry +type SoundEntry struct { + Handle string + Index int + FileName string + Volume byte + GroupSize uint8 + Loop bool + FadeIn uint8 + FadeOut uint8 + DeferInst uint8 + StopInst uint8 + Duration uint8 + Compound int8 + Reverb bool + Falloff uint8 + Cache uint8 + AsyncOnly bool + Priority uint8 + Stream uint8 + Stereo uint8 + Tracking uint8 + Solo uint8 + MusicVol uint8 + Block1 int + Block2 int + Block3 int +} + +// CreateSoundEntry creates a sound entry based on a sound row on sounds.txt +func CreateSoundEntry(soundLine string) SoundEntry { + props := strings.Split(soundLine, "\t") + result := SoundEntry{ + Handle: props[0], + Index: StringToInt(props[1]), + FileName: props[2], + Volume: StringToUint8(props[3]), + GroupSize: StringToUint8(props[4]), + Loop: StringToUint8(props[5]) == 1, + FadeIn: StringToUint8(props[6]), + FadeOut: StringToUint8(props[7]), + DeferInst: StringToUint8(props[8]), + StopInst: StringToUint8(props[9]), + Duration: StringToUint8(props[10]), + Compound: StringToInt8(props[11]), + Reverb: StringToUint8(props[12]) == 1, + Falloff: StringToUint8(props[13]), + Cache: StringToUint8(props[14]), + AsyncOnly: StringToUint8(props[15]) == 1, + Priority: StringToUint8(props[16]), + Stream: StringToUint8(props[17]), + Stereo: StringToUint8(props[18]), + Tracking: StringToUint8(props[19]), + Solo: StringToUint8(props[20]), + MusicVol: StringToUint8(props[21]), + Block1: StringToInt(props[22]), + Block2: StringToInt(props[23]), + Block3: StringToInt(props[24]), + } + return result +} diff --git a/OpenDiablo2/Sprite.go b/OpenDiablo2/Sprite.go new file mode 100644 index 00000000..c99bc118 --- /dev/null +++ b/OpenDiablo2/Sprite.go @@ -0,0 +1,181 @@ +package OpenDiablo2 + +import ( + "encoding/binary" + "time" + + "github.com/hajimehoshi/ebiten" +) + +type Sprite struct { + Directions uint32 + FramesPerDirection uint32 + Frames []SpriteFrame + X, Y int + Frame, Direction uint8 + Blend bool + LastFrameTime time.Time + Animate bool +} + +type SpriteFrame struct { + Flip uint32 + Width uint32 + Height uint32 + OffsetX int32 + OffsetY int32 + Unknown uint32 + NextBlock uint32 + Length uint32 + ImageData []int16 + Image *ebiten.Image +} + +func CreateSprite(data []byte, palette Palette) Sprite { + result := Sprite{ + X: 50, + Y: 50, + Frame: 0, + Direction: 0, + Blend: false, + Directions: binary.LittleEndian.Uint32(data[16:20]), + FramesPerDirection: binary.LittleEndian.Uint32(data[20:24]), + Animate: false, + LastFrameTime: time.Now(), + } + dataPointer := uint32(24) + totalFrames := result.Directions * result.FramesPerDirection + framePointers := make([]uint32, totalFrames) + for i := uint32(0); i < totalFrames; i++ { + framePointers[i] = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + } + result.Frames = make([]SpriteFrame, totalFrames) + for i := uint32(0); i < totalFrames; i++ { + dataPointer = framePointers[i] + + result.Frames[i].Flip = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Width = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Height = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].OffsetX = BytesToInt32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].OffsetY = BytesToInt32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Unknown = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].NextBlock = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].Length = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) + dataPointer += 4 + result.Frames[i].ImageData = make([]int16, result.Frames[i].Width*result.Frames[i].Height) + for fi := range result.Frames[i].ImageData { + result.Frames[i].ImageData[fi] = -1 + } + + x := uint32(0) + y := uint32(result.Frames[i].Height - 1) + for true { + b := data[dataPointer] + dataPointer++ + if b == 0x80 { + if y == 0 { + break + } + y-- + x = 0 + } else if (b & 0x80) > 0 { + transparentPixels := b & 0x7F + for ti := byte(0); ti < transparentPixels; ti++ { + result.Frames[i].ImageData[x+(y*result.Frames[i].Width)+uint32(ti)] = -1 + } + x += uint32(transparentPixels) + } else { + for bi := 0; bi < int(b); bi++ { + result.Frames[i].ImageData[x+(y*result.Frames[i].Width)+uint32(bi)] = int16(data[dataPointer]) + dataPointer++ + } + x += uint32(b) + } + } + result.Frames[i].Image, _ = ebiten.NewImage(int(result.Frames[i].Width), int(result.Frames[i].Height), ebiten.FilterNearest) + newData := make([]byte, result.Frames[i].Width*result.Frames[i].Height*4) + for ii := uint32(0); ii < result.Frames[i].Width*result.Frames[i].Height; ii++ { + if result.Frames[i].ImageData[ii] == -1 { + continue + } + newData[ii*4] = palette.Colors[result.Frames[i].ImageData[ii]].R + newData[(ii*4)+1] = palette.Colors[result.Frames[i].ImageData[ii]].G + newData[(ii*4)+2] = palette.Colors[result.Frames[i].ImageData[ii]].B + newData[(ii*4)+3] = 0xFF + } + + result.Frames[i].Image.ReplacePixels(newData) + } + return result +} + +func (v *Sprite) GetSize() (uint32, uint32) { + frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)] + return frame.Width, frame.Height +} + +func (v *Sprite) updateAnimation() { + if !v.Animate { + return + } + tNow := time.Now() + if v.LastFrameTime.Add(time.Millisecond * 25).After(tNow) { + return + } + v.LastFrameTime = tNow + v.Frame++ + if v.Frame >= uint8(v.FramesPerDirection) { + v.Frame = 0 + } +} + +// Draw draws the sprite onto the target +func (v *Sprite) Draw(target *ebiten.Image) { + v.updateAnimation() + opts := &ebiten.DrawImageOptions{} + frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)] + opts.GeoM.Translate( + float64(int32(v.X)+frame.OffsetX), + float64((int32(v.Y) - int32(frame.Height) + frame.OffsetY)), + ) + if v.Blend { + opts.CompositeMode = ebiten.CompositeModeLighter + } + //opts.ColorM.ChangeHSV(0.0, 1.0, 0.9) + target.DrawImage(frame.Image, opts) +} + +func (v *Sprite) DrawSegments(target *ebiten.Image, xSegments, ySegments, offset int) { + v.updateAnimation() + yOffset := int32(0) + for y := 0; y < ySegments; y++ { + xOffset := int32(0) + biggestYOffset := int32(0) + for x := 0; x < xSegments; x++ { + frame := v.Frames[uint32(x+(y*xSegments)+(offset*xSegments*ySegments))] + opts := &ebiten.DrawImageOptions{} + opts.GeoM.Translate( + float64(int32(v.X)+frame.OffsetX+xOffset), + float64(int32(v.Y)+frame.OffsetY+yOffset), + ) + target.DrawImage(frame.Image, opts) + xOffset += int32(frame.Width) + biggestYOffset = MaxInt32(biggestYOffset, int32(frame.Height)) + } + yOffset += biggestYOffset + } +} + +// MoveTo moves the sprite to the specified coordinates +func (v *Sprite) MoveTo(x, y int) { + v.X = x + v.Y = y +} diff --git a/OpenDiablo2/StringUtils.go b/OpenDiablo2/StringUtils.go new file mode 100644 index 00000000..f45f41c1 --- /dev/null +++ b/OpenDiablo2/StringUtils.go @@ -0,0 +1,36 @@ +package OpenDiablo2 + +import "strconv" + +// StringToInt converts a string to an integer +func StringToInt(text string) int { + result, err := strconv.Atoi(text) + if err != nil { + panic(err) + } + return result +} + +// StringToUint8 converts a string to an uint8 +func StringToUint8(text string) uint8 { + result, err := strconv.Atoi(text) + if err != nil { + panic(err) + } + if result < 0 || result > 255 { + panic("value out of range of byte") + } + return uint8(result) +} + +// StringToInt8 converts a string to an int8 +func StringToInt8(text string) int8 { + result, err := strconv.Atoi(text) + if err != nil { + panic(err) + } + if result < -128 || result > 122 { + panic("value out of range of a signed byte") + } + return int8(result) +} diff --git a/OpenDiablo2/UILabel.go b/OpenDiablo2/UILabel.go new file mode 100644 index 00000000..2002c19b --- /dev/null +++ b/OpenDiablo2/UILabel.go @@ -0,0 +1,80 @@ +package OpenDiablo2 + +import ( + "github.com/hajimehoshi/ebiten" +) + +type UILabel struct { + text string + X int + Y int + Width uint32 + Height uint32 + font *MPQFont + imageData *ebiten.Image +} + +// CreateUILabel creates a new instance of a UI label +func CreateUILabel(engine *Engine, font, palette string) *UILabel { + result := &UILabel{ + font: engine.GetFont(font, palette), + } + + return result +} + +// Draw draws the label on the screen +func (v *UILabel) Draw(target *ebiten.Image) { + if len(v.text) == 0 { + return + } + v.cacheImage() + opts := &ebiten.DrawImageOptions{} + opts.GeoM.Translate(float64(v.X), float64(v.Y)) + opts.CompositeMode = ebiten.CompositeModeSourceAtop + opts.Filter = ebiten.FilterNearest + target.DrawImage(v.imageData, opts) +} + +func (v *UILabel) calculateSize() (uint32, uint32) { + width := uint32(0) + height := uint32(0) + for ch := range v.text { + metric := v.font.Metrics[uint8(ch)] + width += uint32(metric.Width) + height = Max(height, uint32(metric.Height)) + } + return width, height +} + +func (v *UILabel) MoveTo(x, y int) { + v.X = x + v.Y = y +} + +func (v *UILabel) cacheImage() { + if v.imageData != nil { + return + } + width, height := v.calculateSize() + v.Width = width + v.Height = height + v.imageData, _ = ebiten.NewImage(int(width), int(height), ebiten.FilterNearest) + x := uint32(0) + for _, ch := range v.text { + char := uint8(ch) + metric := v.font.Metrics[char] + v.font.FontSprite.Frame = char + v.font.FontSprite.MoveTo(v.X+int(x), int(v.Height)) + v.font.FontSprite.Draw(v.imageData) + x += uint32(metric.Width) + } +} + +func (v *UILabel) SetText(newText string) { + if v.text == newText { + return + } + v.text = newText + v.imageData = nil +} diff --git a/config.json b/config.json new file mode 100644 index 00000000..b9a9b946 --- /dev/null +++ b/config.json @@ -0,0 +1,20 @@ +{ + "FullScreen": false, + "Scale": 1, + "TicksPerSecond": 60, + "RunInBackground": true, + "VsyncEnabled": true, + "MpqPath": "C:/Program Files (x86)/Diablo II", + "MpqLoadOrder": [ + "d2exp.mpq", + "d2xmusic.mpq", + "d2xtalk.mpq", + "d2xvideo.mpq", + "d2data.mpq", + "d2char.mpq", + "d2music.mpq", + "d2sfx.mpq", + "d2video.mpq", + "d2speech.mpq" + ] +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt deleted file mode 100644 index 5f7c87fa..00000000 --- a/src/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -# Initialize the CMake version stuff -cmake_minimum_required(VERSION 3.1...3.13) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${project_SOURCE_DIR}/cmake") - -if(${CMAKE_VERSION} VERSION_LESS 3.12) - cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) -endif() - -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/output) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/output) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/output) - -# Add our nifty CMake scripts ------------------------------------------------------------------------------------------ -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") - -# Prevent users from shooting themselves in the foot ------------------------------------------------------------------- -if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) - message(FATAL_ERROR - "Do not build in-source. - Please remove CMakeCache.txt and the CMakeFiles/ directory. Then build out-of-source.") -endif() - -# ---------------------------------------------------------------------------------------------------------------------- -project(OpenDiablo2 VERSION 0.1 - DESCRIPTION "An open source Diablo2 engine.") -# ---------------------------------------------------------------------------------------------------------------------- - -enable_language(CXX) - -# Freaky relocatable exe stuff ----------------------------------------------------------------------------------------- -set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "") - -set_property(GLOBAL PROPERTY USE_FOLDERS ON) - -# Import the actual projects ------------------------------------------------------------------------------------------- -add_subdirectory(OpenDiablo2.Common) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/OpenDiablo2.Common/include) - -add_subdirectory(OpenDiablo2.SDL2) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/OpenDiablo2.SDL2/include) - -add_subdirectory(OpenDiablo2.Game) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/OpenDiablo2.Game/include) - diff --git a/src/ExtraUtils/CLI11/CLI11.hpp b/src/ExtraUtils/CLI11/CLI11.hpp deleted file mode 100644 index ae58c815..00000000 --- a/src/ExtraUtils/CLI11/CLI11.hpp +++ /dev/null @@ -1,4641 +0,0 @@ -#pragma once - -// CLI11: Version 1.7.1 -// Originally designed by Henry Schreiner -// https://github.com/CLIUtils/CLI11 -// -// This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts -// from: v1.7.1 -// -// From LICENSE: -// -// CLI11 1.7 Copyright (c) 2017-2019 University of Cincinnati, developed by Henry -// Schreiner under NSF AWARD 1414736. All rights reserved. -// -// Redistribution and use in source and binary forms of CLI11, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of the copyright holder nor the names of its contributors -// may be used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -// Standard combined includes: - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -// Verbatim copy from CLI/Version.hpp: - - -#define CLI11_VERSION_MAJOR 1 -#define CLI11_VERSION_MINOR 7 -#define CLI11_VERSION_PATCH 1 -#define CLI11_VERSION "1.7.1" - - - - -// Verbatim copy from CLI/Macros.hpp: - - -// The following version macro is very similar to the one in PyBind11 -#if !(defined(_MSC_VER) && __cplusplus == 199711L) && !defined(__INTEL_COMPILER) -#if __cplusplus >= 201402L -#define CLI11_CPP14 -#if __cplusplus >= 201703L -#define CLI11_CPP17 -#if __cplusplus > 201703L -#define CLI11_CPP20 -#endif -#endif -#endif -#elif defined(_MSC_VER) && __cplusplus == 199711L -// MSVC sets _MSVC_LANG rather than __cplusplus (supposedly until the standard is fully implemented) -// Unless you use the /Zc:__cplusplus flag on Visual Studio 2017 15.7 Preview 3 or newer -#if _MSVC_LANG >= 201402L -#define CLI11_CPP14 -#if _MSVC_LANG > 201402L && _MSC_VER >= 1910 -#define CLI11_CPP17 -#if __MSVC_LANG > 201703L && _MSC_VER >= 1910 -#define CLI11_CPP20 -#endif -#endif -#endif -#endif - -#if defined(CLI11_CPP14) -#define CLI11_DEPRECATED(reason) [[deprecated(reason)]] -#elif defined(_MSC_VER) -#define CLI11_DEPRECATED(reason) __declspec(deprecated(reason)) -#else -#define CLI11_DEPRECATED(reason) __attribute__((deprecated(reason))) -#endif - - - - -// Verbatim copy from CLI/Optional.hpp: - -#ifdef __has_include - -// You can explicitly enable or disable support -// by defining these to 1 or 0. -#if defined(CLI11_CPP17) && __has_include() && \ - !defined(CLI11_STD_OPTIONAL) -#define CLI11_STD_OPTIONAL 1 -#elif !defined(CLI11_STD_OPTIONAL) -#define CLI11_STD_OPTIONAL 0 -#endif - -#if defined(CLI11_CPP14) && __has_include() && \ - !defined(CLI11_EXPERIMENTAL_OPTIONAL) \ - && (!defined(CLI11_STD_OPTIONAL) || CLI11_STD_OPTIONAL == 0) -#define CLI11_EXPERIMENTAL_OPTIONAL 1 -#elif !defined(CLI11_EXPERIMENTAL_OPTIONAL) -#define CLI11_EXPERIMENTAL_OPTIONAL 0 -#endif - -#if __has_include() && !defined(CLI11_BOOST_OPTIONAL) -#include -#if BOOST_VERSION >= 105800 -#define CLI11_BOOST_OPTIONAL 1 -#endif -#elif !defined(CLI11_BOOST_OPTIONAL) -#define CLI11_BOOST_OPTIONAL 0 -#endif - -#endif - -#if CLI11_STD_OPTIONAL -#include -#endif -#if CLI11_EXPERIMENTAL_OPTIONAL -#include -#endif -#if CLI11_BOOST_OPTIONAL -#include -#endif - - -// From CLI/Version.hpp: - - - -// From CLI/Macros.hpp: - - - -// From CLI/Optional.hpp: - -namespace CLI { - -#if CLI11_STD_OPTIONAL -template std::istream &operator>>(std::istream &in, std::optional &val) { - T v; - in >> v; - val = v; - return in; -} -#endif - -#if CLI11_EXPERIMENTAL_OPTIONAL -template std::istream &operator>>(std::istream &in, std::experimental::optional &val) { - T v; - in >> v; - val = v; - return in; -} -#endif - -#if CLI11_BOOST_OPTIONAL -template std::istream &operator>>(std::istream &in, boost::optional &val) { - T v; - in >> v; - val = v; - return in; -} -#endif - -// Export the best optional to the CLI namespace -#if CLI11_STD_OPTIONAL -using std::optional; -#elif CLI11_EXPERIMENTAL_OPTIONAL -using std::experimental::optional; -#elif CLI11_BOOST_OPTIONAL -using boost::optional; -#endif - -// This is true if any optional is found -#if CLI11_STD_OPTIONAL || CLI11_EXPERIMENTAL_OPTIONAL || CLI11_BOOST_OPTIONAL -#define CLI11_OPTIONAL 1 -#endif - -} // namespace CLI - -// From CLI/StringTools.hpp: - -namespace CLI { -namespace detail { - -// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c -/// Split a string by a delim -inline std::vector split(const std::string &s, char delim) { - std::vector elems; - // Check to see if empty string, give consistent result - if(s.empty()) - elems.emplace_back(""); - else { - std::stringstream ss; - ss.str(s); - std::string item; - while(std::getline(ss, item, delim)) { - elems.push_back(item); - } - } - return elems; -} - -/// Simple function to join a string -template std::string join(const T &v, std::string delim = ",") { - std::ostringstream s; - size_t start = 0; - for(const auto &i : v) { - if(start++ > 0) - s << delim; - s << i; - } - return s.str(); -} - -/// Join a string in reverse order -template std::string rjoin(const T &v, std::string delim = ",") { - std::ostringstream s; - for(size_t start = 0; start < v.size(); start++) { - if(start > 0) - s << delim; - s << v[v.size() - start - 1]; - } - return s.str(); -} - -// Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string - -/// Trim whitespace from left of string -inline std::string <rim(std::string &str) { - auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace(ch, std::locale()); }); - str.erase(str.begin(), it); - return str; -} - -/// Trim anything from left of string -inline std::string <rim(std::string &str, const std::string &filter) { - auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); - str.erase(str.begin(), it); - return str; -} - -/// Trim whitespace from right of string -inline std::string &rtrim(std::string &str) { - auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace(ch, std::locale()); }); - str.erase(it.base(), str.end()); - return str; -} - -/// Trim anything from right of string -inline std::string &rtrim(std::string &str, const std::string &filter) { - auto it = - std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); - str.erase(it.base(), str.end()); - return str; -} - -/// Trim whitespace from string -inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); } - -/// Trim anything from string -inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); } - -/// Make a copy of the string and then trim it -inline std::string trim_copy(const std::string &str) { - std::string s = str; - return trim(s); -} - -/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered) -inline std::string trim_copy(const std::string &str, const std::string &filter) { - std::string s = str; - return trim(s, filter); -} -/// Print a two part "help" string -inline std::ostream &format_help(std::ostream &out, std::string name, std::string description, size_t wid) { - name = " " + name; - out << std::setw(static_cast(wid)) << std::left << name; - if(!description.empty()) { - if(name.length() >= wid) - out << "\n" << std::setw(static_cast(wid)) << ""; - out << description; - } - out << "\n"; - return out; -} - -/// Verify the first character of an option -template bool valid_first_char(T c) { return std::isalpha(c, std::locale()) || c == '_'; } - -/// Verify following characters of an option -template bool valid_later_char(T c) { - return std::isalnum(c, std::locale()) || c == '_' || c == '.' || c == '-'; -} - -/// Verify an option name -inline bool valid_name_string(const std::string &str) { - if(str.empty() || !valid_first_char(str[0])) - return false; - for(auto c : str.substr(1)) - if(!valid_later_char(c)) - return false; - return true; -} - -/// Return a lower case version of a string -inline std::string to_lower(std::string str) { - std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) { - return std::tolower(x, std::locale()); - }); - return str; -} - -/// remove underscores from a string -inline std::string remove_underscore(std::string str) { - str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str)); - return str; -} - -/// Find and replace a substring with another substring -inline std::string find_and_replace(std::string str, std::string from, std::string to) { - - size_t start_pos = 0; - - while((start_pos = str.find(from, start_pos)) != std::string::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); - } - - return str; -} - -/// Find a trigger string and call a modify callable function that takes the current string and starting position of the -/// trigger and returns the position in the string to search for the next trigger string -template inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) { - size_t start_pos = 0; - while((start_pos = str.find(trigger, start_pos)) != std::string::npos) { - start_pos = modify(str, start_pos); - } - return str; -} - -/// Split a string '"one two" "three"' into 'one two', 'three' -/// Quote characters can be ` ' or " -inline std::vector split_up(std::string str) { - - const std::string delims("\'\"`"); - auto find_ws = [](char ch) { return std::isspace(ch, std::locale()); }; - trim(str); - - std::vector output; - bool embeddedQuote = false; - char keyChar = ' '; - while(!str.empty()) { - if(delims.find_first_of(str[0]) != std::string::npos) { - keyChar = str[0]; - auto end = str.find_first_of(keyChar, 1); - while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes - end = str.find_first_of(keyChar, end + 1); - embeddedQuote = true; - } - if(end != std::string::npos) { - output.push_back(str.substr(1, end - 1)); - str = str.substr(end + 1); - } else { - output.push_back(str.substr(1)); - str = ""; - } - } else { - auto it = std::find_if(std::begin(str), std::end(str), find_ws); - if(it != std::end(str)) { - std::string value = std::string(str.begin(), it); - output.push_back(value); - str = std::string(it, str.end()); - } else { - output.push_back(str); - str = ""; - } - } - // transform any embedded quotes into the regular character - if(embeddedQuote) { - output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar)); - embeddedQuote = false; - } - trim(str); - } - return output; -} - -/// Add a leader to the beginning of all new lines (nothing is added -/// at the start of the first line). `"; "` would be for ini files -/// -/// Can't use Regex, or this would be a subs. -inline std::string fix_newlines(std::string leader, std::string input) { - std::string::size_type n = 0; - while(n != std::string::npos && n < input.size()) { - n = input.find('\n', n); - if(n != std::string::npos) { - input = input.substr(0, n + 1) + leader + input.substr(n + 1); - n += leader.size(); - } - } - return input; -} - -/// This function detects an equal or colon followed by an escaped quote after an argument -/// then modifies the string to replace the equality with a space. This is needed -/// to allow the split up function to work properly and is intended to be used with the find_and_modify function -/// the return value is the offset+1 which is required by the find_and_modify function. -inline size_t escape_detect(std::string &str, size_t offset) { - auto next = str[offset + 1]; - if((next == '\"') || (next == '\'') || (next == '`')) { - auto astart = str.find_last_of("-/ \"\'`", offset - 1); - if(astart != std::string::npos) { - if(str[astart] == ((str[offset] == '=') ? '-' : '/')) - str[offset] = ' '; // interpret this as a space so the split_up works properly - } - } - return offset + 1; -} - -/// Add quotes if the string contains spaces -inline std::string &add_quotes_if_needed(std::string &str) { - if((str.front() != '"' && str.front() != '\'') || str.front() != str.back()) { - char quote = str.find('"') < str.find('\'') ? '\'' : '"'; - if(str.find(' ') != std::string::npos) { - str.insert(0, 1, quote); - str.append(1, quote); - } - } - return str; -} - -} // namespace detail -} // namespace CLI - -// From CLI/Error.hpp: - -namespace CLI { - -// Use one of these on all error classes. -// These are temporary and are undef'd at the end of this file. -#define CLI11_ERROR_DEF(parent, name) \ - protected: \ - name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \ - name(std::string ename, std::string msg, ExitCodes exit_code) \ - : parent(std::move(ename), std::move(msg), exit_code) {} \ - \ - public: \ - name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \ - name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {} - -// This is added after the one above if a class is used directly and builds its own message -#define CLI11_ERROR_SIMPLE(name) \ - explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {} - -/// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut, -/// int values from e.get_error_code(). -enum class ExitCodes { - Success = 0, - IncorrectConstruction = 100, - BadNameString, - OptionAlreadyAdded, - FileError, - ConversionError, - ValidationError, - RequiredError, - RequiresError, - ExcludesError, - ExtrasError, - ConfigError, - InvalidError, - HorribleError, - OptionNotFound, - ArgumentMismatch, - BaseClass = 127 -}; - -// Error definitions - -/// @defgroup error_group Errors -/// @brief Errors thrown by CLI11 -/// -/// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors. -/// @{ - -/// All errors derive from this one -class Error : public std::runtime_error { - int actual_exit_code; - std::string error_name{"Error"}; - - public: - int get_exit_code() const { return actual_exit_code; } - - std::string get_name() const { return error_name; } - - Error(std::string name, std::string msg, int exit_code = static_cast(ExitCodes::BaseClass)) - : runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {} - - Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast(exit_code)) {} -}; - -// Note: Using Error::Error constructors does not work on GCC 4.7 - -/// Construction errors (not in parsing) -class ConstructionError : public Error { - CLI11_ERROR_DEF(Error, ConstructionError) -}; - -/// Thrown when an option is set to conflicting values (non-vector and multi args, for example) -class IncorrectConstruction : public ConstructionError { - CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction) - CLI11_ERROR_SIMPLE(IncorrectConstruction) - static IncorrectConstruction PositionalFlag(std::string name) { - return IncorrectConstruction(name + ": Flags cannot be positional"); - } - static IncorrectConstruction Set0Opt(std::string name) { - return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead"); - } - static IncorrectConstruction SetFlag(std::string name) { - return IncorrectConstruction(name + ": Cannot set an expected number for flags"); - } - static IncorrectConstruction ChangeNotVector(std::string name) { - return IncorrectConstruction(name + ": You can only change the expected arguments for vectors"); - } - static IncorrectConstruction AfterMultiOpt(std::string name) { - return IncorrectConstruction( - name + ": You can't change expected arguments after you've changed the multi option policy!"); - } - static IncorrectConstruction MissingOption(std::string name) { - return IncorrectConstruction("Option " + name + " is not defined"); - } - static IncorrectConstruction MultiOptionPolicy(std::string name) { - return IncorrectConstruction(name + ": multi_option_policy only works for flags and exact value options"); - } -}; - -/// Thrown on construction of a bad name -class BadNameString : public ConstructionError { - CLI11_ERROR_DEF(ConstructionError, BadNameString) - CLI11_ERROR_SIMPLE(BadNameString) - static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); } - static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); } - static BadNameString DashesOnly(std::string name) { - return BadNameString("Must have a name, not just dashes: " + name); - } - static BadNameString MultiPositionalNames(std::string name) { - return BadNameString("Only one positional name allowed, remove: " + name); - } -}; - -/// Thrown when an option already exists -class OptionAlreadyAdded : public ConstructionError { - CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded) - explicit OptionAlreadyAdded(std::string name) - : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {} - static OptionAlreadyAdded Requires(std::string name, std::string other) { - return OptionAlreadyAdded(name + " requires " + other, ExitCodes::OptionAlreadyAdded); - } - static OptionAlreadyAdded Excludes(std::string name, std::string other) { - return OptionAlreadyAdded(name + " excludes " + other, ExitCodes::OptionAlreadyAdded); - } -}; - -// Parsing errors - -/// Anything that can error in Parse -class ParseError : public Error { - CLI11_ERROR_DEF(Error, ParseError) -}; - -// Not really "errors" - -/// This is a successful completion on parsing, supposed to exit -class Success : public ParseError { - CLI11_ERROR_DEF(ParseError, Success) - Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {} -}; - -/// -h or --help on command line -class CallForHelp : public ParseError { - CLI11_ERROR_DEF(ParseError, CallForHelp) - CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} -}; - -/// Usually something like --help-all on command line -class CallForAllHelp : public ParseError { - CLI11_ERROR_DEF(ParseError, CallForAllHelp) - CallForAllHelp() - : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} -}; - -/// Does not output a diagnostic in CLI11_PARSE, but allows to return from main() with a specific error code. -class RuntimeError : public ParseError { - CLI11_ERROR_DEF(ParseError, RuntimeError) - explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {} -}; - -/// Thrown when parsing an INI file and it is missing -class FileError : public ParseError { - CLI11_ERROR_DEF(ParseError, FileError) - CLI11_ERROR_SIMPLE(FileError) - static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); } -}; - -/// Thrown when conversion call back fails, such as when an int fails to coerce to a string -class ConversionError : public ParseError { - CLI11_ERROR_DEF(ParseError, ConversionError) - CLI11_ERROR_SIMPLE(ConversionError) - ConversionError(std::string member, std::string name) - : ConversionError("The value " + member + " is not an allowed value for " + name) {} - ConversionError(std::string name, std::vector results) - : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {} - static ConversionError TooManyInputsFlag(std::string name) { - return ConversionError(name + ": too many inputs for a flag"); - } - static ConversionError TrueFalse(std::string name) { - return ConversionError(name + ": Should be true/false or a number"); - } -}; - -/// Thrown when validation of results fails -class ValidationError : public ParseError { - CLI11_ERROR_DEF(ParseError, ValidationError) - CLI11_ERROR_SIMPLE(ValidationError) - explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {} -}; - -/// Thrown when a required option is missing -class RequiredError : public ParseError { - CLI11_ERROR_DEF(ParseError, RequiredError) - explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {} - static RequiredError Subcommand(size_t min_subcom) { - if(min_subcom == 1) - return RequiredError("A subcommand"); - else - return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands", - ExitCodes::RequiredError); - } -}; - -/// Thrown when the wrong number of arguments has been received -class ArgumentMismatch : public ParseError { - CLI11_ERROR_DEF(ParseError, ArgumentMismatch) - CLI11_ERROR_SIMPLE(ArgumentMismatch) - ArgumentMismatch(std::string name, int expected, size_t recieved) - : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name + - ", got " + std::to_string(recieved)) - : ("Expected at least " + std::to_string(-expected) + " arguments to " + name + - ", got " + std::to_string(recieved)), - ExitCodes::ArgumentMismatch) {} - - static ArgumentMismatch AtLeast(std::string name, int num) { - return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required"); - } - static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { - return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); - } -}; - -/// Thrown when a requires option is missing -class RequiresError : public ParseError { - CLI11_ERROR_DEF(ParseError, RequiresError) - RequiresError(std::string curname, std::string subname) - : RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {} -}; - -/// Thrown when an excludes option is present -class ExcludesError : public ParseError { - CLI11_ERROR_DEF(ParseError, ExcludesError) - ExcludesError(std::string curname, std::string subname) - : ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {} -}; - -/// Thrown when too many positionals or options are found -class ExtrasError : public ParseError { - CLI11_ERROR_DEF(ParseError, ExtrasError) - explicit ExtrasError(std::vector args) - : ExtrasError((args.size() > 1 ? "The following arguments were not expected: " - : "The following argument was not expected: ") + - detail::rjoin(args, " "), - ExitCodes::ExtrasError) {} -}; - -/// Thrown when extra values are found in an INI file -class ConfigError : public ParseError { - CLI11_ERROR_DEF(ParseError, ConfigError) - CLI11_ERROR_SIMPLE(ConfigError) - static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); } - static ConfigError NotConfigurable(std::string item) { - return ConfigError(item + ": This option is not allowed in a configuration file"); - } -}; - -/// Thrown when validation fails before parsing -class InvalidError : public ParseError { - CLI11_ERROR_DEF(ParseError, InvalidError) - explicit InvalidError(std::string name) - : InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) { - } -}; - -/// This is just a safety check to verify selection and parsing match - you should not ever see it -/// Strings are directly added to this error, but again, it should never be seen. -class HorribleError : public ParseError { - CLI11_ERROR_DEF(ParseError, HorribleError) - CLI11_ERROR_SIMPLE(HorribleError) -}; - -// After parsing - -/// Thrown when counting a non-existent option -class OptionNotFound : public Error { - CLI11_ERROR_DEF(Error, OptionNotFound) - explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {} -}; - -#undef CLI11_ERROR_DEF -#undef CLI11_ERROR_SIMPLE - -/// @} - -} // namespace CLI - -// From CLI/TypeTools.hpp: - -namespace CLI { - -// Type tools - -/// A copy of enable_if_t from C++14, compatible with C++11. -/// -/// We could check to see if C++14 is being used, but it does not hurt to redefine this -/// (even Google does this: https://github.com/google/skia/blob/master/include/private/SkTLogic.h) -/// It is not in the std namespace anyway, so no harm done. - -template using enable_if_t = typename std::enable_if::type; - -/// Check to see if something is a vector (fail check by default) -template struct is_vector { static const bool value = false; }; - -/// Check to see if something is a vector (true if actually a vector) -template struct is_vector> { static bool const value = true; }; - -/// Check to see if something is bool (fail check by default) -template struct is_bool { static const bool value = false; }; - -/// Check to see if something is bool (true if actually a bool) -template <> struct is_bool { static bool const value = true; }; - -namespace detail { -// Based generally on https://rmf.io/cxx11/almost-static-if -/// Simple empty scoped class -enum class enabler {}; - -/// An instance to use in EnableIf -constexpr enabler dummy = {}; - -// Type name print - -/// Was going to be based on -/// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template -/// But this is cleaner and works better in this case - -template ::value && std::is_signed::value, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "INT"; -} - -template ::value && std::is_unsigned::value, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "UINT"; -} - -template ::value, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "FLOAT"; -} - -/// This one should not be used, since vector types print the internal type -template ::value, detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "VECTOR"; -} - -template ::value && !std::is_integral::value && !is_vector::value, - detail::enabler> = detail::dummy> -constexpr const char *type_name() { - return "TEXT"; -} - -// Lexical cast - -/// Signed integers / enums -template ::value && std::is_signed::value), detail::enabler> = detail::dummy> -bool lexical_cast(std::string input, T &output) { - try { - size_t n = 0; - long long output_ll = std::stoll(input, &n, 0); - output = static_cast(output_ll); - return n == input.size() && static_cast(output) == output_ll; - } catch(const std::invalid_argument &) { - return false; - } catch(const std::out_of_range &) { - return false; - } -} - -/// Unsigned integers -template ::value && std::is_unsigned::value, detail::enabler> = detail::dummy> -bool lexical_cast(std::string input, T &output) { - if(!input.empty() && input.front() == '-') - return false; // std::stoull happily converts negative values to junk without any errors. - - try { - size_t n = 0; - unsigned long long output_ll = std::stoull(input, &n, 0); - output = static_cast(output_ll); - return n == input.size() && static_cast(output) == output_ll; - } catch(const std::invalid_argument &) { - return false; - } catch(const std::out_of_range &) { - return false; - } -} - -/// Floats -template ::value, detail::enabler> = detail::dummy> -bool lexical_cast(std::string input, T &output) { - try { - size_t n = 0; - output = static_cast(std::stold(input, &n)); - return n == input.size(); - } catch(const std::invalid_argument &) { - return false; - } catch(const std::out_of_range &) { - return false; - } -} - -/// String and similar -template ::value && !std::is_integral::value && - std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_cast(std::string input, T &output) { - output = input; - return true; -} - -/// Non-string parsable -template ::value && !std::is_integral::value && - !std::is_assignable::value, - detail::enabler> = detail::dummy> -bool lexical_cast(std::string input, T &output) { - std::istringstream is; - - is.str(input); - is >> output; - return !is.fail() && !is.rdbuf()->in_avail(); -} - -} // namespace detail -} // namespace CLI - -// From CLI/Split.hpp: - -namespace CLI { -namespace detail { - -// Returns false if not a short option. Otherwise, sets opt name and rest and returns true -inline bool split_short(const std::string ¤t, std::string &name, std::string &rest) { - if(current.size() > 1 && current[0] == '-' && valid_first_char(current[1])) { - name = current.substr(1, 1); - rest = current.substr(2); - return true; - } else - return false; -} - -// Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true -inline bool split_long(const std::string ¤t, std::string &name, std::string &value) { - if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { - auto loc = current.find_first_of('='); - if(loc != std::string::npos) { - name = current.substr(2, loc - 2); - value = current.substr(loc + 1); - } else { - name = current.substr(2); - value = ""; - } - return true; - } else - return false; -} - -// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true -inline bool split_windows(const std::string ¤t, std::string &name, std::string &value) { - if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) { - auto loc = current.find_first_of(':'); - if(loc != std::string::npos) { - name = current.substr(1, loc - 1); - value = current.substr(loc + 1); - } else { - name = current.substr(1); - value = ""; - } - return true; - } else - return false; -} - -// Splits a string into multiple long and short names -inline std::vector split_names(std::string current) { - std::vector output; - size_t val; - while((val = current.find(",")) != std::string::npos) { - output.push_back(trim_copy(current.substr(0, val))); - current = current.substr(val + 1); - } - output.push_back(trim_copy(current)); - return output; -} - -/// Get a vector of short names, one of long names, and a single name -inline std::tuple, std::vector, std::string> -get_names(const std::vector &input) { - - std::vector short_names; - std::vector long_names; - std::string pos_name; - - for(std::string name : input) { - if(name.length() == 0) - continue; - else if(name.length() > 1 && name[0] == '-' && name[1] != '-') { - if(name.length() == 2 && valid_first_char(name[1])) - short_names.emplace_back(1, name[1]); - else - throw BadNameString::OneCharName(name); - } else if(name.length() > 2 && name.substr(0, 2) == "--") { - name = name.substr(2); - if(valid_name_string(name)) - long_names.push_back(name); - else - throw BadNameString::BadLongName(name); - } else if(name == "-" || name == "--") { - throw BadNameString::DashesOnly(name); - } else { - if(pos_name.length() > 0) - throw BadNameString::MultiPositionalNames(name); - pos_name = name; - } - } - - return std::tuple, std::vector, std::string>( - short_names, long_names, pos_name); -} - -} // namespace detail -} // namespace CLI - -// From CLI/ConfigFwd.hpp: - -namespace CLI { - -class App; - -namespace detail { - -/// Comma separated join, adds quotes if needed -inline std::string ini_join(std::vector args) { - std::ostringstream s; - size_t start = 0; - for(const auto &arg : args) { - if(start++ > 0) - s << " "; - - auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace(ch, std::locale()); }); - if(it == arg.end()) - s << arg; - else if(arg.find(R"(")") == std::string::npos) - s << R"(")" << arg << R"(")"; - else - s << R"(')" << arg << R"(')"; - } - - return s.str(); -} - -} // namespace detail - -/// Holds values to load into Options -struct ConfigItem { - /// This is the list of parents - std::vector parents; - - /// This is the name - std::string name; - - /// Listing of inputs - std::vector inputs; - - /// The list of parents and name joined by "." - std::string fullname() const { - std::vector tmp = parents; - tmp.emplace_back(name); - return detail::join(tmp, "."); - } -}; - -/// This class provides a converter for configuration files. -class Config { - protected: - std::vector items; - - public: - /// Convert an app into a configuration - virtual std::string to_config(const App *, bool, bool, std::string) const = 0; - - /// Convert a configuration into an app - virtual std::vector from_config(std::istream &) const = 0; - - /// Convert a flag to a bool - virtual std::vector to_flag(const ConfigItem &item) const { - if(item.inputs.size() == 1) { - std::string val = item.inputs.at(0); - val = detail::to_lower(val); - - if(val == "true" || val == "on" || val == "yes") { - return std::vector(1); - } else if(val == "false" || val == "off" || val == "no") { - return std::vector(); - } else { - try { - size_t ui = std::stoul(val); - return std::vector(ui); - } catch(const std::invalid_argument &) { - throw ConversionError::TrueFalse(item.fullname()); - } - } - } else { - throw ConversionError::TooManyInputsFlag(item.fullname()); - } - } - - /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure - std::vector from_file(const std::string &name) { - std::ifstream input{name}; - if(!input.good()) - throw FileError::Missing(name); - - return from_config(input); - } - - /// virtual destructor - virtual ~Config() = default; -}; - -/// This converter works with INI files -class ConfigINI : public Config { - public: - std::string to_config(const App *, bool default_also, bool write_description, std::string prefix) const override; - - std::vector from_config(std::istream &input) const override { - std::string line; - std::string section = "default"; - - std::vector output; - - while(getline(input, line)) { - std::vector items_buffer; - - detail::trim(line); - size_t len = line.length(); - if(len > 1 && line[0] == '[' && line[len - 1] == ']') { - section = line.substr(1, len - 2); - } else if(len > 0 && line[0] != ';') { - output.emplace_back(); - ConfigItem &out = output.back(); - - // Find = in string, split and recombine - auto pos = line.find('='); - if(pos != std::string::npos) { - out.name = detail::trim_copy(line.substr(0, pos)); - std::string item = detail::trim_copy(line.substr(pos + 1)); - items_buffer = detail::split_up(item); - } else { - out.name = detail::trim_copy(line); - items_buffer = {"ON"}; - } - - if(detail::to_lower(section) != "default") { - out.parents = {section}; - } - - if(out.name.find('.') != std::string::npos) { - std::vector plist = detail::split(out.name, '.'); - out.name = plist.back(); - plist.pop_back(); - out.parents.insert(out.parents.end(), plist.begin(), plist.end()); - } - - out.inputs.insert(std::end(out.inputs), std::begin(items_buffer), std::end(items_buffer)); - } - } - return output; - } -}; - -} // namespace CLI - -// From CLI/Validators.hpp: - -namespace CLI { - -/// @defgroup validator_group Validators - -/// @brief Some validators that are provided -/// -/// These are simple `std::string(const std::string&)` validators that are useful. They return -/// a string if the validation fails. A custom struct is provided, as well, with the same user -/// semantics, but with the ability to provide a new type name. -/// @{ - -/// -struct Validator { - /// This is the type name, if empty the type name will not be changed - std::string tname; - - /// This it the base function that is to be called. - /// Returns a string error message if validation fails. - std::function func; - - /// This is the required operator for a validator - provided to help - /// users (CLI11 uses the member `func` directly) - std::string operator()(const std::string &str) const { return func(str); }; - - /// Combining validators is a new validator - Validator operator&(const Validator &other) const { - Validator newval; - newval.tname = (tname == other.tname ? tname : ""); - - // Give references (will make a copy in lambda function) - const std::function &f1 = func; - const std::function &f2 = other.func; - - newval.func = [f1, f2](const std::string &filename) { - std::string s1 = f1(filename); - std::string s2 = f2(filename); - if(!s1.empty() && !s2.empty()) - return s1 + " & " + s2; - else - return s1 + s2; - }; - return newval; - } - - /// Combining validators is a new validator - Validator operator|(const Validator &other) const { - Validator newval; - newval.tname = (tname == other.tname ? tname : ""); - - // Give references (will make a copy in lambda function) - const std::function &f1 = func; - const std::function &f2 = other.func; - - newval.func = [f1, f2](const std::string &filename) { - std::string s1 = f1(filename); - std::string s2 = f2(filename); - if(s1.empty() || s2.empty()) - return std::string(); - else - return s1 + " & " + s2; - }; - return newval; - } -}; - -// The implementation of the built in validators is using the Validator class; -// the user is only expected to use the const (static) versions (since there's no setup). -// Therefore, this is in detail. -namespace detail { - -/// Check for an existing file (returns error message if check fails) -struct ExistingFileValidator : public Validator { - ExistingFileValidator() { - tname = "FILE"; - func = [](const std::string &filename) { - struct stat buffer; - bool exist = stat(filename.c_str(), &buffer) == 0; - bool is_dir = (buffer.st_mode & S_IFDIR) != 0; - if(!exist) { - return "File does not exist: " + filename; - } else if(is_dir) { - return "File is actually a directory: " + filename; - } - return std::string(); - }; - } -}; - -/// Check for an existing directory (returns error message if check fails) -struct ExistingDirectoryValidator : public Validator { - ExistingDirectoryValidator() { - tname = "DIR"; - func = [](const std::string &filename) { - struct stat buffer; - bool exist = stat(filename.c_str(), &buffer) == 0; - bool is_dir = (buffer.st_mode & S_IFDIR) != 0; - if(!exist) { - return "Directory does not exist: " + filename; - } else if(!is_dir) { - return "Directory is actually a file: " + filename; - } - return std::string(); - }; - } -}; - -/// Check for an existing path -struct ExistingPathValidator : public Validator { - ExistingPathValidator() { - tname = "PATH"; - func = [](const std::string &filename) { - struct stat buffer; - bool const exist = stat(filename.c_str(), &buffer) == 0; - if(!exist) { - return "Path does not exist: " + filename; - } - return std::string(); - }; - } -}; - -/// Check for an non-existing path -struct NonexistentPathValidator : public Validator { - NonexistentPathValidator() { - tname = "PATH"; - func = [](const std::string &filename) { - struct stat buffer; - bool exist = stat(filename.c_str(), &buffer) == 0; - if(exist) { - return "Path already exists: " + filename; - } - return std::string(); - }; - } -}; -} // namespace detail - -// Static is not needed here, because global const implies static. - -/// Check for existing file (returns error message if check fails) -const detail::ExistingFileValidator ExistingFile; - -/// Check for an existing directory (returns error message if check fails) -const detail::ExistingDirectoryValidator ExistingDirectory; - -/// Check for an existing path -const detail::ExistingPathValidator ExistingPath; - -/// Check for an non-existing path -const detail::NonexistentPathValidator NonexistentPath; - -/// Produce a range (factory). Min and max are inclusive. -struct Range : public Validator { - /// This produces a range with min and max inclusive. - /// - /// Note that the constructor is templated, but the struct is not, so C++17 is not - /// needed to provide nice syntax for Range(a,b). - template Range(T min, T max) { - std::stringstream out; - out << detail::type_name() << " in [" << min << " - " << max << "]"; - - tname = out.str(); - func = [min, max](std::string input) { - T val; - detail::lexical_cast(input, val); - if(val < min || val > max) - return "Value " + input + " not in range " + std::to_string(min) + " to " + std::to_string(max); - - return std::string(); - }; - } - - /// Range of one value is 0 to value - template explicit Range(T max) : Range(static_cast(0), max) {} -}; - -namespace detail { -/// split a string into a program name and command line arguments -/// the string is assumed to contain a file name followed by other arguments -/// the return value contains is a pair with the first argument containing the program name and the second everything -/// else -inline std::pair split_program_name(std::string commandline) { - // try to determine the programName - std::pair vals; - trim(commandline); - auto esp = commandline.find_first_of(' ', 1); - while(!ExistingFile(commandline.substr(0, esp)).empty()) { - esp = commandline.find_first_of(' ', esp + 1); - if(esp == std::string::npos) { - // if we have reached the end and haven't found a valid file just assume the first argument is the - // program name - esp = commandline.find_first_of(' ', 1); - break; - } - } - vals.first = commandline.substr(0, esp); - rtrim(vals.first); - // strip the program name - vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{}; - ltrim(vals.second); - return vals; -} -} // namespace detail -/// @} - -} // namespace CLI - -// From CLI/FormatterFwd.hpp: - -namespace CLI { - -class Option; -class App; - -/// This enum signifies the type of help requested -/// -/// This is passed in by App; all user classes must accept this as -/// the second argument. - -enum class AppFormatMode { - Normal, //< The normal, detailed help - All, //< A fully expanded help - Sub, //< Used when printed as part of expanded subcommand -}; - -/// This is the minimum requirements to run a formatter. -/// -/// A user can subclass this is if they do not care at all -/// about the structure in CLI::Formatter. -class FormatterBase { - protected: - /// @name Options - ///@{ - - /// The width of the first column - size_t column_width_{30}; - - /// @brief The required help printout labels (user changeable) - /// Values are Needs, Excludes, etc. - std::map labels_; - - ///@} - /// @name Basic - ///@{ - - public: - FormatterBase() = default; - FormatterBase(const FormatterBase &) = default; - FormatterBase(FormatterBase &&) = default; - virtual ~FormatterBase() = default; - - /// This is the key method that puts together help - virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0; - - ///@} - /// @name Setters - ///@{ - - /// Set the "REQUIRED" label - void label(std::string key, std::string val) { labels_[key] = val; } - - /// Set the column width - void column_width(size_t val) { column_width_ = val; } - - ///@} - /// @name Getters - ///@{ - - /// Get the current value of a name (REQUIRED, etc.) - std::string get_label(std::string key) const { - if(labels_.find(key) == labels_.end()) - return key; - else - return labels_.at(key); - } - - /// Get the current column width - size_t get_column_width() const { return column_width_; } - - ///@} -}; - -/// This is a specialty override for lambda functions -class FormatterLambda final : public FormatterBase { - using funct_t = std::function; - - /// The lambda to hold and run - funct_t lambda_; - - public: - /// Create a FormatterLambda with a lambda function - explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} - - /// This will simply call the lambda function - std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { - return lambda_(app, name, mode); - } -}; - -/// This is the default Formatter for CLI11. It pretty prints help output, and is broken into quite a few -/// overridable methods, to be highly customizable with minimal effort. -class Formatter : public FormatterBase { - public: - Formatter() = default; - Formatter(const Formatter &) = default; - Formatter(Formatter &&) = default; - - /// @name Overridables - ///@{ - - /// This prints out a group of options with title - /// - virtual std::string make_group(std::string group, bool is_positional, std::vector opts) const; - - /// This prints out just the positionals "group" - virtual std::string make_positionals(const App *app) const; - - /// This prints out all the groups of options - std::string make_groups(const App *app, AppFormatMode mode) const; - - /// This prints out all the subcommands - virtual std::string make_subcommands(const App *app, AppFormatMode mode) const; - - /// This prints out a subcommand - virtual std::string make_subcommand(const App *sub) const; - - /// This prints out a subcommand in help-all - virtual std::string make_expanded(const App *sub) const; - - /// This prints out all the groups of options - virtual std::string make_footer(const App *app) const; - - /// This displays the description line - virtual std::string make_description(const App *app) const; - - /// This displays the usage line - virtual std::string make_usage(const App *app, std::string name) const; - - /// This puts everything together - std::string make_help(const App *, std::string, AppFormatMode) const override; - - ///@} - /// @name Options - ///@{ - - /// This prints out an option help line, either positional or optional form - virtual std::string make_option(const Option *opt, bool is_positional) const { - std::stringstream out; - detail::format_help( - out, make_option_name(opt, is_positional) + make_option_opts(opt), make_option_desc(opt), column_width_); - return out.str(); - } - - /// @brief This is the name part of an option, Default: left column - virtual std::string make_option_name(const Option *, bool) const; - - /// @brief This is the options part of the name, Default: combined into left column - virtual std::string make_option_opts(const Option *) const; - - /// @brief This is the description. Default: Right column, on new line if left column too large - virtual std::string make_option_desc(const Option *) const; - - /// @brief This is used to print the name on the USAGE line - virtual std::string make_option_usage(const Option *opt) const; - - ///@} -}; - -} // namespace CLI - -// From CLI/Option.hpp: - -namespace CLI { - -using results_t = std::vector; -using callback_t = std::function; - -class Option; -class App; - -using Option_p = std::unique_ptr