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

View File

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

View File

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

View File

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

View File

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

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,
"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",