Minor changes to support localized versions.

This commit is contained in:
Tim Sarbin 2019-11-01 00:21:39 -04:00
parent bf0412554f
commit da5baec685
8 changed files with 195 additions and 113 deletions

98
Common/Sounds.go Normal file
View File

@ -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))
}

View File

@ -25,6 +25,7 @@ import (
// EngineConfig defines the configuration for the engine, loaded from config.json // EngineConfig defines the configuration for the engine, loaded from config.json
type EngineConfig struct { type EngineConfig struct {
Language string
FullScreen bool FullScreen bool
Scale float64 Scale float64
RunInBackground bool RunInBackground bool
@ -36,12 +37,17 @@ type EngineConfig struct {
BgmVolume float64 BgmVolume float64
} }
type MpqFileRecord struct {
MpqFile string
IsPatch bool
UnpatchedMpqFile string
}
// Engine is the core OpenDiablo2 engine // Engine is the core OpenDiablo2 engine
type Engine struct { type Engine struct {
Settings *EngineConfig // Engine configuration settings from json file 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 Palettes map[Palettes.Palette]Common.Palette // Color palettes
SoundEntries map[string]Sound.SoundEntry // Sound configurations
LoadingSprite *Common.Sprite // The sprite shown when loading stuff 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. 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 stepLoadingSize float64 // The size for each loading step
@ -59,12 +65,13 @@ func CreateEngine() *Engine {
nextScene: nil, nextScene: nil,
} }
result.loadConfigurationFile() result.loadConfigurationFile()
ResourcePaths.LanguageCode = result.Settings.Language
result.mapMpqFiles() result.mapMpqFiles()
result.loadPalettes() result.loadPalettes()
result.loadSoundEntries()
Common.LoadTextDictionary(result) Common.LoadTextDictionary(result)
Common.LoadLevelTypes(result) Common.LoadLevelTypes(result)
Common.LoadLevelPresets(result) Common.LoadLevelPresets(result)
Common.LoadSounds(result)
result.SoundManager = Sound.CreateManager(result) result.SoundManager = Sound.CreateManager(result)
result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume) result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume)
result.UIManager = UI.CreateManager(result, *result.SoundManager) result.UIManager = UI.CreateManager(result, *result.SoundManager)
@ -106,8 +113,7 @@ func (v *Engine) loadConfigurationFile() {
func (v *Engine) mapMpqFiles() { func (v *Engine) mapMpqFiles() {
log.Println("mapping mpq file structure") log.Println("mapping mpq file structure")
v.Files = make(map[string]string) v.Files = make(map[string]*MpqFileRecord)
lock := sync.RWMutex{}
for _, mpqFileName := range v.Settings.MpqLoadOrder { for _, mpqFileName := range v.Settings.MpqLoadOrder {
mpqPath := path.Join(v.Settings.MpqPath, mpqFileName) mpqPath := path.Join(v.Settings.MpqPath, mpqFileName)
mpq, err := MPQ.Load(mpqPath) mpq, err := MPQ.Load(mpqPath)
@ -115,16 +121,20 @@ func (v *Engine) mapMpqFiles() {
log.Fatal(err) log.Fatal(err)
} }
fileListText, err := mpq.ReadFile("(listfile)") fileListText, err := mpq.ReadFile("(listfile)")
if err != nil { if err != nil || fileListText == nil {
log.Fatal(err) // Super secret patch file activate!
continue
} }
fileList := strings.Split(string(fileListText), "\r\n") fileList := strings.Split(string(fileListText), "\r\n")
for _, filePath := range fileList { for _, filePath := range fileList {
if _, exists := v.Files[strings.ToLower(filePath)]; exists { 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 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 // LoadFile loads a file from the specified mpq and returns the data as a byte array
func (v *Engine) LoadFile(fileName string) []byte { func (v *Engine) LoadFile(fileName string) []byte {
fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode)
mutex.Lock() mutex.Lock()
// TODO: May want to cache some things if performance becomes an issue // TODO: May want to cache some things if performance becomes an issue
mpqFile := v.Files[strings.ToLower(fileName)] mpqFile := v.Files[strings.ToLower(fileName)]
mpq, err := MPQ.Load(mpqFile) var mpq MPQ.MPQ
if err != nil { var err error
log.Printf("Error loading file '%s'", fileName) if mpqFile == nil {
log.Fatal(err) // 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:] fileName = strings.ReplaceAll(fileName, `/`, `\`)[1:]
blockTableEntry, err := mpq.GetFileBlockData(fileName) blockTableEntry, err := mpq.GetFileBlockData(fileName)
if err != nil { 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 // LoadSprite loads a sprite from the game's data files
func (v *Engine) LoadSprite(fileName string, palette Palettes.Palette) *Common.Sprite { func (v *Engine) LoadSprite(fileName string, palette Palettes.Palette) *Common.Sprite {
data := v.LoadFile(fileName) data := v.LoadFile(fileName)

View File

@ -7,6 +7,8 @@ import (
"os" "os"
"path" "path"
"strings" "strings"
"github.com/essial/OpenDiablo2/ResourcePaths"
) )
// MPQ represents an MPQ archive // MPQ represents an MPQ archive
@ -40,6 +42,13 @@ type HashTableEntry struct { // 16 bytes
BlockIndex uint32 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 // FileFlag represents flags for a file record in the MPQ archive
type FileFlag uint32 type FileFlag uint32
@ -216,6 +225,7 @@ func (v MPQ) getFileHashEntry(fileName string) (HashTableEntry, error) {
// GetFileBlockData gets a block table entry // GetFileBlockData gets a block table entry
func (v MPQ) GetFileBlockData(fileName string) (BlockTableEntry, error) { func (v MPQ) GetFileBlockData(fileName string) (BlockTableEntry, error) {
fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode)
fileEntry, err := v.getFileHashEntry(fileName) fileEntry, err := v.getFileHashEntry(fileName)
if err != nil { if err != nil {
return BlockTableEntry{}, err 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 // ReadFile reads a file from the MPQ and returns a memory stream
func (v MPQ) ReadFile(fileName string) ([]byte, error) { func (v MPQ) ReadFile(fileName string) ([]byte, error) {
fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode)
fileBlockData, err := v.GetFileBlockData(fileName) fileBlockData, err := v.GetFileBlockData(fileName)
if err != nil { if err != nil {
log.Panic(err) return []byte{}, err
} }
fileBlockData.FileName = strings.ToLower(fileName) fileBlockData.FileName = strings.ToLower(fileName)
fileBlockData.calculateEncryptionSeed() fileBlockData.calculateEncryptionSeed()
@ -247,6 +264,7 @@ func (v MPQ) ReadFile(fileName string) ([]byte, error) {
// ReadTextFile reads a file and returns it as a string // ReadTextFile reads a file and returns it as a string
func (v MPQ) ReadTextFile(fileName string) (string, error) { func (v MPQ) ReadTextFile(fileName string) (string, error) {
fileName = strings.ReplaceAll(fileName, "{LANG}", ResourcePaths.LanguageCode)
data, err := v.ReadFile(fileName) data, err := v.ReadFile(fileName)
if err != nil { if err != nil {
return "", err return "", err
@ -270,6 +288,5 @@ func (v MPQ) GetFileList() ([]string, error) {
return nil, err return nil, err
} }
log.Printf("File Contents:\n%s", strings.TrimRight(string(data), "\x00")) log.Printf("File Contents:\n%s", strings.TrimRight(string(data), "\x00"))
data = nil
return []string{""}, nil return []string{""}, nil
} }

View File

@ -40,6 +40,10 @@ func CreateStream(mpq MPQ, blockTableEntry BlockTableEntry, fileName string) *St
} }
result.BlockSize = 0x200 << result.MPQData.Data.BlockSize 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) { if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) && !result.BlockTableEntry.HasFlag(FileSingleUnit) {
result.loadBlockOffsets() result.loadBlockOffsets()
} }

View File

@ -1,5 +1,7 @@
package ResourcePaths package ResourcePaths
var LanguageCode string
const ( const (
// --- Screens --- // --- Screens ---
@ -17,7 +19,7 @@ const (
// --- Credits --- // --- Credits ---
CreditsBackground = "/data/global/ui/CharSelect/creditsbckgexpand.dc6" CreditsBackground = "/data/global/ui/CharSelect/creditsbckgexpand.dc6"
CreditsText = "/data/local/ui/eng/ExpansionCredits.txt" CreditsText = "/data/local/ui/{LANG}/ExpansionCredits.txt"
// --- Character Select Screen --- // --- Character Select Screen ---
@ -156,9 +158,9 @@ const (
// --- Data --- // --- Data ---
ExpansionStringTable = "/data/local/lng/eng/expansionstring.tbl" ExpansionStringTable = "/data/local/lng/{LANG}/expansionstring.tbl"
StringTable = "/data/local/lng/eng/string.tbl" StringTable = "/data/local/lng/{LANG}/string.tbl"
PatchStringTable = "/data/local/lng/eng/patchstring.tbl" PatchStringTable = "/data/local/lng/{LANG}/patchstring.tbl"
LevelPreset = "/data/global/excel/LvlPrest.bin" LevelPreset = "/data/global/excel/LvlPrest.bin"
LevelType = "/data/global/excel/LvlTypes.bin" LevelType = "/data/global/excel/LvlTypes.bin"
LevelDetails = "/data/global/excel/Levels.bin" LevelDetails = "/data/global/excel/Levels.bin"
@ -224,21 +226,21 @@ const (
// --- Sound Effects --- // --- Sound Effects ---
SFXButtonClick = "/data/global/sfx/Cursor/button.wav" SFXButtonClick = "ESOUND_CURSOR_BUTTON_CLICK"
SFXAmazonDeselect = "/data/global/sfx/Cursor/intro/amazon deselect.wav" SFXAmazonDeselect = "ESOUND_CURSOR_AMAZON_DESELECT"
SFXAmazonSelect = "/data/global/sfx/Cursor/intro/amazon select.wav" SFXAmazonSelect = "ESOUND_CURSOR_AMAZON_SELECT"
SFXAssassinDeselect = "/data/global/sfx/Cursor/intro/assassin deselect.wav" SFXAssassinDeselect = "/data/global/sfx/Cursor/intro/assassin deselect.wav"
SFXAssassinSelect = "/data/global/sfx/Cursor/intro/assassin select.wav" SFXAssassinSelect = "/data/global/sfx/Cursor/intro/assassin select.wav"
SFXBarbarianDeselect = "/data/global/sfx/Cursor/intro/barbarian deselect.wav" SFXBarbarianDeselect = "ESOUND_CURSOR_BARBARIAN_DESELECT"
SFXBarbarianSelect = "/data/global/sfx/Cursor/intro/barbarian select.wav" SFXBarbarianSelect = "ESOUND_CURSOR_BARBARIAN_SELECT"
SFXDruidDeselect = "/data/global/sfx/Cursor/intro/druid deselect.wav" SFXDruidDeselect = "/data/global/sfx/Cursor/intro/druid deselect.wav"
SFXDruidSelect = "/data/global/sfx/Cursor/intro/druid select.wav" SFXDruidSelect = "/data/global/sfx/Cursor/intro/druid select.wav"
SFXNecromancerDeselect = "/data/global/sfx/Cursor/intro/necromancer deselect.wav" SFXNecromancerDeselect = "ESOUND_CURSOR_NECROMANCER_DESELECT"
SFXNecromancerSelect = "/data/global/sfx/Cursor/intro/necromancer select.wav" SFXNecromancerSelect = "ESOUND_CURSOR_NECROMANCER_SELECT"
SFXPaladinDeselect = "/data/global/sfx/Cursor/intro/paladin deselect.wav" SFXPaladinDeselect = "ESOUND_CURSOR_PALADIN_DESELECT"
SFXPaladinSelect = "/data/global/sfx/Cursor/intro/paladin select.wav" SFXPaladinSelect = "ESOUND_CURSOR_PALADIN_SELECT"
SFXSorceressDeselect = "/data/global/sfx/Cursor/intro/sorceress deselect.wav" SFXSorceressDeselect = "ESOUND_CURSOR_SORCERESS_DESELECT"
SFXSorceressSelect = "/data/global/sfx/Cursor/intro/sorceress select.wav" SFXSorceressSelect = "ESOUND_CURSOR_SORCERESS_SELECT"
// --- Enemy Data --- // --- Enemy Data ---

View File

@ -16,8 +16,14 @@ type SoundEffect struct {
func CreateSoundEffect(sfx string, fileProvider Common.FileProvider, context *audio.Context, volume float64) *SoundEffect { func CreateSoundEffect(sfx string, fileProvider Common.FileProvider, context *audio.Context, volume float64) *SoundEffect {
result := &SoundEffect{} result := &SoundEffect{}
var soundFile string
audioData := fileProvider.LoadFile(sfx) 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)) d, err := wav.Decode(context, audio.BytesReadSeekCloser(audioData))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -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
}

View File

@ -1,4 +1,5 @@
{ {
"Language": "ENG",
"FullScreen": false, "FullScreen": false,
"Scale": 1, "Scale": 1,
"TicksPerSecond": 60, "TicksPerSecond": 60,
@ -8,6 +9,7 @@
"BgmVolume": 0.3, "BgmVolume": 0.3,
"MpqPath": "C:/Program Files (x86)/Diablo II", "MpqPath": "C:/Program Files (x86)/Diablo II",
"MpqLoadOrder": [ "MpqLoadOrder": [
"Patch_D2.mpq",
"d2exp.mpq", "d2exp.mpq",
"d2xmusic.mpq", "d2xmusic.mpq",
"d2xtalk.mpq", "d2xtalk.mpq",