diff --git a/Common/Sounds.go b/Common/Sounds.go new file mode 100644 index 00000000..6c80676f --- /dev/null +++ b/Common/Sounds.go @@ -0,0 +1,98 @@ +package Common + +import ( + "log" + "strings" + + "github.com/essial/OpenDiablo2/ResourcePaths" +) + +// 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 +} + +var Sounds map[string]SoundEntry + +func LoadSounds(fileProvider FileProvider) { + Sounds = make(map[string]SoundEntry) + soundData := strings.Split(string(fileProvider.LoadFile(ResourcePaths.SoundSettings)), "\r\n")[1:] + for _, line := range soundData { + if len(line) == 0 { + continue + } + soundEntry := createSoundEntry(line) + soundEntry.FileName = "/data/global/sfx/" + strings.ReplaceAll(soundEntry.FileName, `\`, "/") + Sounds[soundEntry.Handle] = soundEntry + /* + // Use the following code to write out the values + f, err := os.OpenFile(`C:\Users\lunat\Desktop\D2\sounds.txt`, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Println(err) + } + defer f.Close() + if _, err := f.WriteString("\n[" + soundEntry.Handle + "] " + soundEntry.FileName); err != nil { + log.Println(err) + } + */ + } + log.Println("Loaded %d sound definitions", len(Sounds)) +} diff --git a/Core/Engine.go b/Core/Engine.go index fab8c8f4..c4d76b72 100644 --- a/Core/Engine.go +++ b/Core/Engine.go @@ -25,6 +25,7 @@ import ( // EngineConfig defines the configuration for the engine, loaded from config.json type EngineConfig struct { + Language string FullScreen bool Scale float64 RunInBackground bool @@ -36,12 +37,17 @@ type EngineConfig struct { BgmVolume float64 } +type MpqFileRecord struct { + MpqFile string + IsPatch bool + UnpatchedMpqFile 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 + Files map[string]*MpqFileRecord // Map that defines which files are in which MPQs Palettes map[Palettes.Palette]Common.Palette // Color palettes - SoundEntries map[string]Sound.SoundEntry // Sound configurations LoadingSprite *Common.Sprite // The sprite shown when loading stuff loadingProgress float64 // LoadingProcess is a range between 0.0 and 1.0. If set, loading screen displays. stepLoadingSize float64 // The size for each loading step @@ -59,12 +65,13 @@ func CreateEngine() *Engine { nextScene: nil, } result.loadConfigurationFile() + ResourcePaths.LanguageCode = result.Settings.Language result.mapMpqFiles() result.loadPalettes() - result.loadSoundEntries() Common.LoadTextDictionary(result) Common.LoadLevelTypes(result) Common.LoadLevelPresets(result) + Common.LoadSounds(result) result.SoundManager = Sound.CreateManager(result) result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume) result.UIManager = UI.CreateManager(result, *result.SoundManager) @@ -106,8 +113,7 @@ func (v *Engine) loadConfigurationFile() { func (v *Engine) mapMpqFiles() { log.Println("mapping mpq file structure") - v.Files = make(map[string]string) - lock := sync.RWMutex{} + v.Files = make(map[string]*MpqFileRecord) for _, mpqFileName := range v.Settings.MpqLoadOrder { mpqPath := path.Join(v.Settings.MpqPath, mpqFileName) mpq, err := MPQ.Load(mpqPath) @@ -115,16 +121,20 @@ func (v *Engine) mapMpqFiles() { log.Fatal(err) } fileListText, err := mpq.ReadFile("(listfile)") - if err != nil { - log.Fatal(err) + if err != nil || fileListText == nil { + // Super secret patch file activate! + continue } fileList := strings.Split(string(fileListText), "\r\n") + for _, filePath := range fileList { if _, exists := v.Files[strings.ToLower(filePath)]; exists { - lock.RUnlock() + if v.Files[strings.ToLower(filePath)].IsPatch { + v.Files[strings.ToLower(filePath)].UnpatchedMpqFile = mpqPath + } continue } - v.Files[`/`+strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)] = mpqPath + v.Files[`/`+strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)] = &MpqFileRecord{mpqPath, false, ""} } } } @@ -133,14 +143,38 @@ var mutex sync.Mutex // LoadFile loads a file from the specified mpq and returns the data as a byte array func (v *Engine) LoadFile(fileName string) []byte { + fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode) mutex.Lock() // TODO: May want to cache some things if performance becomes an issue mpqFile := v.Files[strings.ToLower(fileName)] - mpq, err := MPQ.Load(mpqFile) - if err != nil { - log.Printf("Error loading file '%s'", fileName) - log.Fatal(err) + var mpq MPQ.MPQ + var err error + if mpqFile == nil { + // Super secret non-listed file? + for _, mpqFile := range v.Settings.MpqLoadOrder { + mpqFilePath := path.Join(v.Settings.MpqPath, mpqFile) + mpq, err = MPQ.Load(mpqFilePath) + newFileName := strings.ReplaceAll(fileName, `/`, `\`)[1:] + if err != nil { + continue + } + if !mpq.FileExists(newFileName) { + continue + } + // We found the super-secret file! + v.Files[strings.ToLower(fileName)] = &MpqFileRecord{mpqFilePath, false, ""} + break + } + } else if mpqFile.IsPatch { + log.Fatal("Tried to load a patchfile") + } else { + mpq, err = MPQ.Load(mpqFile.MpqFile) + if err != nil { + log.Printf("Error loading file '%s'", fileName) + log.Fatal(err) + } } + fileName = strings.ReplaceAll(fileName, `/`, `\`)[1:] blockTableEntry, err := mpq.GetFileBlockData(fileName) if err != nil { @@ -173,19 +207,6 @@ func (v *Engine) loadPalettes() { } } -func (v *Engine) loadSoundEntries() { - log.Println("loading sound configurations") - v.SoundEntries = make(map[string]Sound.SoundEntry) - soundData := strings.Split(string(v.LoadFile(ResourcePaths.SoundSettings)), "\r\n")[1:] - for _, line := range soundData { - if len(line) == 0 { - continue - } - soundEntry := Sound.CreateSoundEntry(line) - v.SoundEntries[soundEntry.Handle] = soundEntry - } -} - // LoadSprite loads a sprite from the game's data files func (v *Engine) LoadSprite(fileName string, palette Palettes.Palette) *Common.Sprite { data := v.LoadFile(fileName) diff --git a/MPQ/MPQ.go b/MPQ/MPQ.go index e293450a..961eac69 100644 --- a/MPQ/MPQ.go +++ b/MPQ/MPQ.go @@ -7,6 +7,8 @@ import ( "os" "path" "strings" + + "github.com/essial/OpenDiablo2/ResourcePaths" ) // MPQ represents an MPQ archive @@ -40,6 +42,13 @@ type HashTableEntry struct { // 16 bytes BlockIndex uint32 } +type PatchInfo struct { + Length uint32 // Length of patch info header, in bytes + Flags uint32 // Flags. 0x80000000 = MD5 (?) + DataSize uint32 // Uncompressed size of the patch file + Md5 [16]byte // MD5 of the entire patch file after decompression +} + // FileFlag represents flags for a file record in the MPQ archive type FileFlag uint32 @@ -216,6 +225,7 @@ func (v MPQ) getFileHashEntry(fileName string) (HashTableEntry, error) { // GetFileBlockData gets a block table entry func (v MPQ) GetFileBlockData(fileName string) (BlockTableEntry, error) { + fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode) fileEntry, err := v.getFileHashEntry(fileName) if err != nil { return BlockTableEntry{}, err @@ -231,11 +241,18 @@ func (v *MPQ) Close() { } } +func (v MPQ) FileExists(fileName string) bool { + fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode) + _, err := v.getFileHashEntry(fileName) + return err == nil +} + // ReadFile reads a file from the MPQ and returns a memory stream func (v MPQ) ReadFile(fileName string) ([]byte, error) { + fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode) fileBlockData, err := v.GetFileBlockData(fileName) if err != nil { - log.Panic(err) + return []byte{}, err } fileBlockData.FileName = strings.ToLower(fileName) fileBlockData.calculateEncryptionSeed() @@ -247,6 +264,7 @@ func (v MPQ) ReadFile(fileName string) ([]byte, error) { // ReadTextFile reads a file and returns it as a string func (v MPQ) ReadTextFile(fileName string) (string, error) { + fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode) data, err := v.ReadFile(fileName) if err != nil { return "", err @@ -270,6 +288,5 @@ func (v MPQ) GetFileList() ([]string, error) { return nil, err } log.Printf("File Contents:\n%s", strings.TrimRight(string(data), "\x00")) - data = nil return []string{""}, nil } diff --git a/MPQ/MPQStream.go b/MPQ/MPQStream.go index 0a7f1154..ffc8eb99 100644 --- a/MPQ/MPQStream.go +++ b/MPQ/MPQStream.go @@ -40,6 +40,10 @@ func CreateStream(mpq MPQ, blockTableEntry BlockTableEntry, fileName string) *St } result.BlockSize = 0x200 << result.MPQData.Data.BlockSize + if result.BlockTableEntry.HasFlag(FilePatchFile) { + log.Fatal("Patching is not supported") + } + if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) && !result.BlockTableEntry.HasFlag(FileSingleUnit) { result.loadBlockOffsets() } diff --git a/ResourcePaths/ResourcePaths.go b/ResourcePaths/ResourcePaths.go index 171347ed..d96b4c5a 100644 --- a/ResourcePaths/ResourcePaths.go +++ b/ResourcePaths/ResourcePaths.go @@ -1,5 +1,7 @@ package ResourcePaths +var LanguageCode string + const ( // --- Screens --- @@ -17,7 +19,7 @@ const ( // --- Credits --- CreditsBackground = "/data/global/ui/CharSelect/creditsbckgexpand.dc6" - CreditsText = "/data/local/ui/eng/ExpansionCredits.txt" + CreditsText = "/data/local/ui/{LANG}/ExpansionCredits.txt" // --- Character Select Screen --- @@ -156,9 +158,9 @@ const ( // --- Data --- - ExpansionStringTable = "/data/local/lng/eng/expansionstring.tbl" - StringTable = "/data/local/lng/eng/string.tbl" - PatchStringTable = "/data/local/lng/eng/patchstring.tbl" + ExpansionStringTable = "/data/local/lng/{LANG}/expansionstring.tbl" + StringTable = "/data/local/lng/{LANG}/string.tbl" + PatchStringTable = "/data/local/lng/{LANG}/patchstring.tbl" LevelPreset = "/data/global/excel/LvlPrest.bin" LevelType = "/data/global/excel/LvlTypes.bin" LevelDetails = "/data/global/excel/Levels.bin" @@ -224,21 +226,21 @@ const ( // --- 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" + SFXButtonClick = "ESOUND_CURSOR_BUTTON_CLICK" + SFXAmazonDeselect = "ESOUND_CURSOR_AMAZON_DESELECT" + SFXAmazonSelect = "ESOUND_CURSOR_AMAZON_SELECT" 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" + SFXBarbarianDeselect = "ESOUND_CURSOR_BARBARIAN_DESELECT" + SFXBarbarianSelect = "ESOUND_CURSOR_BARBARIAN_SELECT" 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" + SFXNecromancerDeselect = "ESOUND_CURSOR_NECROMANCER_DESELECT" + SFXNecromancerSelect = "ESOUND_CURSOR_NECROMANCER_SELECT" + SFXPaladinDeselect = "ESOUND_CURSOR_PALADIN_DESELECT" + SFXPaladinSelect = "ESOUND_CURSOR_PALADIN_SELECT" + SFXSorceressDeselect = "ESOUND_CURSOR_SORCERESS_DESELECT" + SFXSorceressSelect = "ESOUND_CURSOR_SORCERESS_SELECT" // --- Enemy Data --- diff --git a/Sound/SoundEffect.go b/Sound/SoundEffect.go index 7588358c..ada21829 100644 --- a/Sound/SoundEffect.go +++ b/Sound/SoundEffect.go @@ -16,8 +16,14 @@ type SoundEffect struct { func CreateSoundEffect(sfx string, fileProvider Common.FileProvider, context *audio.Context, volume float64) *SoundEffect { result := &SoundEffect{} - - audioData := fileProvider.LoadFile(sfx) + var soundFile string + if _, exists := Common.Sounds[sfx]; exists { + soundEntry := Common.Sounds[sfx] + soundFile = soundEntry.FileName + } else { + soundFile = sfx + } + audioData := fileProvider.LoadFile(soundFile) d, err := wav.Decode(context, audio.BytesReadSeekCloser(audioData)) if err != nil { log.Fatal(err) diff --git a/Sound/SoundEntry.go b/Sound/SoundEntry.go deleted file mode 100644 index ddc73d6d..00000000 --- a/Sound/SoundEntry.go +++ /dev/null @@ -1,68 +0,0 @@ -package Sound - -import ( - "github.com/essial/OpenDiablo2/Common" - "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: Common.StringToInt(props[1]), - FileName: props[2], - Volume: Common.StringToUint8(props[3]), - GroupSize: Common.StringToUint8(props[4]), - Loop: Common.StringToUint8(props[5]) == 1, - FadeIn: Common.StringToUint8(props[6]), - FadeOut: Common.StringToUint8(props[7]), - DeferInst: Common.StringToUint8(props[8]), - StopInst: Common.StringToUint8(props[9]), - Duration: Common.StringToUint8(props[10]), - Compound: Common.StringToInt8(props[11]), - Reverb: Common.StringToUint8(props[12]) == 1, - Falloff: Common.StringToUint8(props[13]), - Cache: Common.StringToUint8(props[14]), - AsyncOnly: Common.StringToUint8(props[15]) == 1, - Priority: Common.StringToUint8(props[16]), - Stream: Common.StringToUint8(props[17]), - Stereo: Common.StringToUint8(props[18]), - Tracking: Common.StringToUint8(props[19]), - Solo: Common.StringToUint8(props[20]), - MusicVol: Common.StringToUint8(props[21]), - Block1: Common.StringToInt(props[22]), - Block2: Common.StringToInt(props[23]), - Block3: Common.StringToInt(props[24]), - } - return result -} diff --git a/config.json b/config.json index 2a39dedf..6efa2d72 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,5 @@ { + "Language": "ENG", "FullScreen": false, "Scale": 1, "TicksPerSecond": 60, @@ -8,6 +9,7 @@ "BgmVolume": 0.3, "MpqPath": "C:/Program Files (x86)/Diablo II", "MpqLoadOrder": [ + "Patch_D2.mpq", "d2exp.mpq", "d2xmusic.mpq", "d2xtalk.mpq",