mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-20 07:27:19 -05:00
Minor changes to support localized versions.
This commit is contained in:
parent
bf0412554f
commit
da5baec685
98
Common/Sounds.go
Normal file
98
Common/Sounds.go
Normal 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))
|
||||
}
|
@ -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)
|
||||
|
21
MPQ/MPQ.go
21
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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 ---
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user