diff --git a/Common/StreamReader.go b/Common/StreamReader.go index 51c253d2..bb81842c 100644 --- a/Common/StreamReader.go +++ b/Common/StreamReader.go @@ -39,16 +39,16 @@ func (v *StreamReader) GetByte() byte { return result } -// GetWord returns a uint16 word from the stream -func (v *StreamReader) GetWord() uint16 { +// GetUInt16 returns a uint16 word from the stream +func (v *StreamReader) GetUInt16() uint16 { result := uint16(v.data[v.position]) result += uint16(v.data[v.position+1]) << 8 v.position += 2 return result } -// GetSWord returns a int16 word from the stream -func (v *StreamReader) GetSWord() int16 { +// GetInt16 returns a int16 word from the stream +func (v *StreamReader) GetInt16() int16 { var result int16 err := binary.Read(bytes.NewReader([]byte{v.data[v.position], v.data[v.position+1]}), binary.LittleEndian, &result) if err != nil { @@ -58,12 +58,23 @@ func (v *StreamReader) GetSWord() int16 { return result } -// GetDword returns a uint32 dword from the stream -func (v *StreamReader) GetDword() uint32 { - result := uint32(v.data[v.position]) - result += uint32(v.data[v.position+1]) << 8 - result += uint32(v.data[v.position+2]) << 16 - result += uint32(v.data[v.position+3]) << 24 +func (v *StreamReader) SetPosition(newPosition uint64) { + v.position = newPosition +} + +// GetUInt32 returns a uint32 word from the stream +func (v *StreamReader) GetUInt32() uint32 { + var result uint32 + err := binary.Read(bytes.NewReader( + []byte{ + v.data[v.position], + v.data[v.position+1], + v.data[v.position+2], + v.data[v.position+3], + }), binary.LittleEndian, &result) + if err != nil { + log.Panic(err) + } v.position += 4 return result } @@ -73,6 +84,15 @@ func (v *StreamReader) ReadByte() (byte, error) { return v.GetByte(), nil } +// ReadBytes reads multiple bytes +func (v *StreamReader) ReadBytes(count int) ([]byte, error) { + result := make([]byte, count) + for i := 0; i < count; i++ { + result[i] = v.GetByte() + } + return result, nil +} + // Read implements io.Reader func (v *StreamReader) Read(p []byte) (n int, err error) { streamLength := v.GetSize() @@ -86,3 +106,7 @@ func (v *StreamReader) Read(p []byte) (n int, err error) { p[i] = v.GetByte() } } + +func (v *StreamReader) Eof() bool { + return v.position >= uint64(len(v.data)) +} diff --git a/Common/StringUtils.go b/Common/StringUtils.go index f412d22d..9b00ee5c 100644 --- a/Common/StringUtils.go +++ b/Common/StringUtils.go @@ -1,6 +1,13 @@ package Common -import "strconv" +import ( + "bytes" + "fmt" + "strconv" + "strings" + "unicode/utf16" + "unicode/utf8" +) // StringToInt converts a string to an integer func StringToInt(text string) int { @@ -34,3 +41,50 @@ func StringToInt8(text string) int8 { } return int8(result) } + +func Utf16BytesToString(b []byte) (string, error) { + + if len(b)%2 != 0 { + return "", fmt.Errorf("Must have even length byte slice") + } + + u16s := make([]uint16, 1) + + ret := &bytes.Buffer{} + + b8buf := make([]byte, 4) + + lb := len(b) + for i := 0; i < lb; i += 2 { + u16s[0] = uint16(b[i]) + (uint16(b[i+1]) << 8) + r := utf16.Decode(u16s) + n := utf8.EncodeRune(b8buf, r[0]) + ret.Write(b8buf[:n]) + } + + return ret.String(), nil +} + +func SplitIntoLinesWithMaxWidth(fullSentence string, maxChars int) []string { + lines := make([]string, 0) + line := "" + totalLength := 0 + words := strings.Split(fullSentence, " ") + for _, word := range words { + totalLength += 1 + len(word) + if totalLength > maxChars { + totalLength = len(word) + lines = append(lines, line) + line = "" + } else { + line += " " + } + line += word + } + + if len(line) > 0 { + lines = append(lines, line) + } + + return lines +} diff --git a/Common/TextDictionary.go b/Common/TextDictionary.go new file mode 100644 index 00000000..06efcd16 --- /dev/null +++ b/Common/TextDictionary.go @@ -0,0 +1,103 @@ +package Common + +import ( + "log" + "strconv" + + "github.com/essial/OpenDiablo2/ResourcePaths" +) + +type textDictionaryHashEntry struct { + IsActive bool + Index uint16 + HashValue uint32 + IndexString uint32 + NameString uint32 + NameLength uint16 +} + +var lookupTable map[string]string + +func TranslateString(key string) string { + result, ok := lookupTable[key] + if !ok { + log.Panic("Could not find a string for the key '%s'", key) + } + return result +} + +func LoadTextDictionary(fileProvider FileProvider) { + lookupTable = make(map[string]string) + + loadDictionary(fileProvider, ResourcePaths.PatchStringTable) + loadDictionary(fileProvider, ResourcePaths.ExpansionStringTable) + loadDictionary(fileProvider, ResourcePaths.StringTable) + log.Printf("Loaded %d entries from the string table", len(lookupTable)) +} + +func loadDictionary(fileProvider FileProvider, dictionaryName string) { + dictionaryData := fileProvider.LoadFile(dictionaryName) + br := CreateStreamReader(dictionaryData) + br.ReadBytes(2) // CRC + numberOfElements := br.GetUInt16() + hashTableSize := br.GetUInt32() + br.ReadByte() // Version (always 0) + br.GetUInt32() // StringOffset + br.GetUInt32() // When the number of times you have missed a match with a hash key equals this value, you give up because it is not there. + br.GetUInt32() // FileSize + + elementIndex := make([]uint16, numberOfElements) + for i := 0; i < int(numberOfElements); i++ { + elementIndex[i] = br.GetUInt16() + } + + hashEntries := make([]textDictionaryHashEntry, hashTableSize) + for i := 0; i < int(hashTableSize); i++ { + hashEntries[i] = textDictionaryHashEntry{ + br.GetByte() == 1, + br.GetUInt16(), + br.GetUInt32(), + br.GetUInt32(), + br.GetUInt32(), + br.GetUInt16(), + } + } + + for idx, hashEntry := range hashEntries { + if !hashEntry.IsActive { + continue + } + br.SetPosition(uint64(hashEntry.NameString)) + nameVal, _ := br.ReadBytes(int(hashEntry.NameLength - 1)) + value := string(nameVal) + br.SetPosition(uint64(hashEntry.IndexString)) + key := "" + for true { + b := br.GetByte() + if b == 0 { + break + } + key += string(b) + } + if key == "x" || key == "X" { + key = "#" + strconv.Itoa(idx) + } + _, exists := lookupTable[key] + if !exists { + lookupTable[key] = value + + } + // Use the following code to write out the values + /* + f, err := os.OpenFile(`C:\Users\lunat\Desktop\D2\langdict.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[" + key + "] " + value); err != nil { + log.Println(err) + } + */ + } +} diff --git a/Compression/Wav.go b/Compression/Wav.go index 50f19c9d..91008e22 100644 --- a/Compression/Wav.go +++ b/Compression/Wav.go @@ -37,7 +37,7 @@ func WavDecompress(data []byte, channelCount int) []byte { shift := input.GetByte() for i := 0; i < channelCount; i++ { - temp := input.GetSWord() + temp := input.GetInt16() Array2[i] = int(temp) output.PushSWord(temp) } diff --git a/Core/Engine.go b/Core/Engine.go index a2f4d62b..dac5b244 100644 --- a/Core/Engine.go +++ b/Core/Engine.go @@ -60,6 +60,7 @@ func CreateEngine() *Engine { result.mapMpqFiles() result.loadPalettes() result.loadSoundEntries() + Common.LoadTextDictionary(result) result.SoundManager = Sound.CreateManager(result) result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume) result.UIManager = UI.CreateManager(result, *result.SoundManager) diff --git a/ResourcePaths/ResourcePaths.go b/ResourcePaths/ResourcePaths.go index e5366431..634e15b5 100644 --- a/ResourcePaths/ResourcePaths.go +++ b/ResourcePaths/ResourcePaths.go @@ -156,8 +156,9 @@ const ( // --- Data --- - EnglishTable = "/data/local/lng/eng/English.txt" ExpansionStringTable = "/data/local/lng/eng/expansionstring.tbl" + StringTable = "/data/local/lng/eng/string.tbl" + PatchStringTable = "/data/local/lng/eng/patchstring.tbl" LevelPreset = "/data/global/excel/LvlPrest.txt" LevelType = "/data/global/excel/LvlTypes.txt" LevelDetails = "/data/global/excel/Levels.txt" diff --git a/Scenes/Credits.go b/Scenes/Credits.go index bff3adf6..cb13130e 100644 --- a/Scenes/Credits.go +++ b/Scenes/Credits.go @@ -1,12 +1,8 @@ package Scenes import ( - "bytes" - "fmt" "image/color" "strings" - "unicode/utf16" - "unicode/utf8" "github.com/essial/OpenDiablo2/Common" "github.com/essial/OpenDiablo2/Palettes" @@ -52,29 +48,6 @@ func CreateCredits(fileProvider Common.FileProvider, sceneProvider SceneProvider return result } -func utf16BytesToString(b []byte) (string, error) { - - if len(b)%2 != 0 { - return "", fmt.Errorf("Must have even length byte slice") - } - - u16s := make([]uint16, 1) - - ret := &bytes.Buffer{} - - b8buf := make([]byte, 4) - - lb := len(b) - for i := 0; i < lb; i += 2 { - u16s[0] = uint16(b[i]) + (uint16(b[i+1]) << 8) - r := utf16.Decode(u16s) - n := utf8.EncodeRune(b8buf, r[0]) - ret.Write(b8buf[:n]) - } - - return ret.String(), nil -} - // Load is called to load the resources for the credits scene func (v *Credits) Load() []func() { return []func(){ @@ -89,7 +62,7 @@ func (v *Credits) Load() []func() { v.uiManager.AddWidget(v.exitButton) }, func() { - fileData, _ := utf16BytesToString(v.fileProvider.LoadFile(ResourcePaths.CreditsText)[2:]) + fileData, _ := Common.Utf16BytesToString(v.fileProvider.LoadFile(ResourcePaths.CreditsText)[2:]) v.creditsText = strings.Split(fileData, "\r\n") for i := range v.creditsText { v.creditsText[i] = strings.Trim(v.creditsText[i], " ") diff --git a/Scenes/MainMenu.go b/Scenes/MainMenu.go index b77bf1a8..5b407591 100644 --- a/Scenes/MainMenu.go +++ b/Scenes/MainMenu.go @@ -69,7 +69,7 @@ func (v *MainMenu) Load() []func() { func() { v.copyrightLabel2 = UI.CreateLabel(v.fileProvider, ResourcePaths.FontFormal12, Palettes.Static) v.copyrightLabel2.Alignment = UI.LabelAlignCenter - v.copyrightLabel2.SetText("All Rights Reserved.") + v.copyrightLabel2.SetText(Common.TranslateString("#1614")) v.copyrightLabel2.Color = color.RGBA{188, 168, 140, 255} v.copyrightLabel2.MoveTo(400, 525) }, @@ -109,27 +109,27 @@ func (v *MainMenu) Load() []func() { v.diabloLogoRightBack.MoveTo(400, 120) }, func() { - v.exitDiabloButton = UI.CreateButton(UI.ButtonTypeWide, v.fileProvider, "EXIT DIABLO II") + v.exitDiabloButton = UI.CreateButton(UI.ButtonTypeWide, v.fileProvider, Common.TranslateString("#1625")) v.exitDiabloButton.MoveTo(264, 535) v.exitDiabloButton.SetVisible(!v.ShowTrademarkScreen) v.exitDiabloButton.OnActivated(func() { v.onExitButtonClicked() }) v.uiManager.AddWidget(v.exitDiabloButton) }, func() { - v.creditsButton = UI.CreateButton(UI.ButtonTypeShort, v.fileProvider, "CREDITS") + v.creditsButton = UI.CreateButton(UI.ButtonTypeShort, v.fileProvider, Common.TranslateString("#1627")) v.creditsButton.MoveTo(264, 505) v.creditsButton.SetVisible(!v.ShowTrademarkScreen) v.creditsButton.OnActivated(func() { v.onCreditsButtonClicked() }) v.uiManager.AddWidget(v.creditsButton) }, func() { - v.cinematicsButton = UI.CreateButton(UI.ButtonTypeShort, v.fileProvider, "CINEMATICS") + v.cinematicsButton = UI.CreateButton(UI.ButtonTypeShort, v.fileProvider, Common.TranslateString("#1639")) v.cinematicsButton.MoveTo(401, 505) v.cinematicsButton.SetVisible(!v.ShowTrademarkScreen) v.uiManager.AddWidget(v.cinematicsButton) }, func() { - v.singlePlayerButton = UI.CreateButton(UI.ButtonTypeWide, v.fileProvider, "SINGLE PLAYER") + v.singlePlayerButton = UI.CreateButton(UI.ButtonTypeWide, v.fileProvider, Common.TranslateString("#1620")) v.singlePlayerButton.MoveTo(264, 290) v.singlePlayerButton.SetVisible(!v.ShowTrademarkScreen) v.singlePlayerButton.OnActivated(func() { v.onSinglePlayerClicked() }) diff --git a/Scenes/SelectHeroClass.go b/Scenes/SelectHeroClass.go index 219a3d97..156f5e9b 100644 --- a/Scenes/SelectHeroClass.go +++ b/Scenes/SelectHeroClass.go @@ -44,7 +44,12 @@ type SelectHeroClass struct { bgImage *Common.Sprite campfire *Common.Sprite headingLabel *UI.Label + heroClassLabel *UI.Label + heroDesc1Label *UI.Label + heroDesc2Label *UI.Label + heroDesc3Label *UI.Label heroRenderInfo map[Common.Hero]*HeroRenderInfo + selectedHero Common.Hero } func CreateSelectHeroClass( @@ -58,6 +63,7 @@ func CreateSelectHeroClass( fileProvider: fileProvider, soundManager: soundManager, heroRenderInfo: make(map[Common.Hero]*HeroRenderInfo), + selectedHero: Common.HeroNone, } return result } @@ -76,6 +82,26 @@ func (v *SelectHeroClass) Load() []func() { v.headingLabel.SetText("Select Hero Class") v.headingLabel.Alignment = UI.LabelAlignCenter }, + func() { + v.heroClassLabel = UI.CreateLabel(v.fileProvider, ResourcePaths.Font30, Palettes.Units) + v.heroClassLabel.Alignment = UI.LabelAlignCenter + v.heroClassLabel.MoveTo(400, 65) + }, + func() { + v.heroDesc1Label = UI.CreateLabel(v.fileProvider, ResourcePaths.Font16, Palettes.Units) + v.heroDesc1Label.Alignment = UI.LabelAlignCenter + v.heroDesc1Label.MoveTo(400, 100) + }, + func() { + v.heroDesc2Label = UI.CreateLabel(v.fileProvider, ResourcePaths.Font16, Palettes.Units) + v.heroDesc2Label.Alignment = UI.LabelAlignCenter + v.heroDesc2Label.MoveTo(400, 115) + }, + func() { + v.heroDesc3Label = UI.CreateLabel(v.fileProvider, ResourcePaths.Font16, Palettes.Units) + v.heroDesc3Label.Alignment = UI.LabelAlignCenter + v.heroDesc3Label.MoveTo(400, 130) + }, func() { v.campfire = v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectCampfire, Palettes.Fechar) v.campfire.MoveTo(380, 335) @@ -335,6 +361,12 @@ func (v *SelectHeroClass) Unload() { func (v *SelectHeroClass) Render(screen *ebiten.Image) { v.bgImage.DrawSegments(screen, 4, 3, 0) v.headingLabel.Draw(screen) + if v.selectedHero != Common.HeroNone { + v.heroClassLabel.Draw(screen) + v.heroDesc1Label.Draw(screen) + v.heroDesc2Label.Draw(screen) + v.heroDesc3Label.Draw(screen) + } for heroClass, heroInfo := range v.heroRenderInfo { if heroInfo.Stance == HeroStanceIdle || heroInfo.Stance == HeroStanceIdleSelected { v.renderHero(screen, heroClass) @@ -356,9 +388,16 @@ func (v *SelectHeroClass) Update(tickTime float64) { break } } - for heroType := range v.heroRenderInfo { + allIdle := true + for heroType, data := range v.heroRenderInfo { + if allIdle && data.Stance != HeroStanceIdle { + allIdle = false + } v.updateHeroSelectionHover(heroType, canSelect) } + if v.selectedHero != Common.HeroNone && allIdle { + v.selectedHero = Common.HeroNone + } } func (v *SelectHeroClass) updateHeroSelectionHover(hero Common.Hero, canSelect bool) { @@ -409,8 +448,8 @@ func (v *SelectHeroClass) updateHeroSelectionHover(hero Common.Hero, canSelect b heroInfo.BackWalkSpriteOverlay.ResetAnimation() } } - // selectedHero = hero; - // UpdateHeroText(); + v.selectedHero = hero + v.updateHeroText() renderInfo.SelectSfx.Play() return @@ -422,13 +461,10 @@ func (v *SelectHeroClass) updateHeroSelectionHover(hero Common.Hero, canSelect b renderInfo.Stance = HeroStanceIdle } - /* - if (selectedHero == null && mouseHover) - { - selectedHero = hero; - UpdateHeroText(); - } - */ + if v.selectedHero == Common.HeroNone && mouseHover { + v.selectedHero = hero + v.updateHeroText() + } } @@ -456,3 +492,65 @@ func (v *SelectHeroClass) renderHero(screen *ebiten.Image, hero Common.Hero) { } } } + +func (v *SelectHeroClass) updateHeroText() { + switch v.selectedHero { + case Common.HeroNone: + return + case Common.HeroBarbarian: + v.heroClassLabel.SetText(Common.TranslateString("partycharbar")) + v.setDescLabels("#1709") + case Common.HeroNecromancer: + v.heroClassLabel.SetText(Common.TranslateString("partycharnec")) + v.setDescLabels("#1704") + case Common.HeroPaladin: + v.heroClassLabel.SetText(Common.TranslateString("partycharpal")) + v.setDescLabels("#1711") + case Common.HeroAssassin: + v.heroClassLabel.SetText(Common.TranslateString("partycharass")) + v.setDescLabels("#305") + case Common.HeroSorceress: + v.heroClassLabel.SetText(Common.TranslateString("partycharsor")) + v.setDescLabels("#1710") + case Common.HeroAmazon: + v.heroClassLabel.SetText(Common.TranslateString("partycharama")) + v.setDescLabels("#1698") + case Common.HeroDruid: + v.heroClassLabel.SetText(Common.TranslateString("partychardru")) + v.setDescLabels("#304") + } + /* + if (selectedHero == null) + return; + + switch (selectedHero.Value) + { + + } + + heroClassLabel.Location = new Point(400 - (heroClassLabel.TextArea.Width / 2), 65); + heroDesc1Label.Location = new Point(400 - (heroDesc1Label.TextArea.Width / 2), 100); + heroDesc2Label.Location = new Point(400 - (heroDesc2Label.TextArea.Width / 2), 115); + heroDesc3Label.Location = new Point(400 - (heroDesc3Label.TextArea.Width / 2), 130); + */ +} + +func (v *SelectHeroClass) setDescLabels(descKey string) { + heroDesc := Common.TranslateString(descKey) + parts := Common.SplitIntoLinesWithMaxWidth(heroDesc, 37) + if len(parts) > 1 { + v.heroDesc1Label.SetText(parts[0]) + } else { + v.heroDesc1Label.SetText("") + } + if len(parts) > 1 { + v.heroDesc2Label.SetText(parts[1]) + } else { + v.heroDesc2Label.SetText("") + } + if len(parts) > 2 { + v.heroDesc3Label.SetText(parts[2]) + } else { + v.heroDesc3Label.SetText("") + } +} diff --git a/main.go b/main.go index 5bb39630..09efb289 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,10 @@ package main import ( - "github.com/essial/OpenDiablo2/Core" "log" + "github.com/essial/OpenDiablo2/Core" + "github.com/essial/OpenDiablo2/MPQ" "github.com/hajimehoshi/ebiten" )