diff --git a/.travis.yml b/.travis.yml index 9fe41c99..b6a547b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ go: - 1.13.3 before_install: - sudo apt-get -y install libx11-dev mesa-common-dev libglfw3-dev libgles2-mesa-dev libasound2-dev -script: buildci.sh +script: go get && golangci-lint run . && go build ./cmd/Client git: depth: 1 notifications: diff --git a/Common/BitStream.go b/Common/BitStream.go index 925c7b75..942d43ff 100644 --- a/Common/BitStream.go +++ b/Common/BitStream.go @@ -50,9 +50,9 @@ func (v *BitStream) EnsureBits(bitCount int) bool { if v.dataPosition >= len(v.data) { return false } - nextvalue := v.data[v.dataPosition] + nextValue := v.data[v.dataPosition] v.dataPosition++ - v.current |= int(nextvalue) << v.bitCount + v.current |= int(nextValue) << v.bitCount v.bitCount += 8 return true } diff --git a/Common/Math.go b/Common/Math.go index da4f0471..e6aeafd0 100644 --- a/Common/Math.go +++ b/Common/Math.go @@ -24,6 +24,14 @@ func MaxInt32(a, b int32) int32 { return b } +// MinInt32 returns the higher of two values +func MinInt32(a, b int32) int32 { + if a < b { + return a + } + return b +} + // BytesToInt32 converts 4 bytes to int32 func BytesToInt32(b []byte) int32 { // equivalnt of return int32(binary.LittleEndian.Uint32(b)) diff --git a/Common/StreamReader.go b/Common/StreamReader.go index 0634f0dd..51c253d2 100644 --- a/Common/StreamReader.go +++ b/Common/StreamReader.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "io" + "log" ) // StreamReader allows you to read data from a byte array in various formats @@ -41,7 +42,7 @@ func (v *StreamReader) GetByte() byte { // GetWord returns a uint16 word from the stream func (v *StreamReader) GetWord() uint16 { result := uint16(v.data[v.position]) - result += (uint16(v.data[v.position+1]) << 8) + result += uint16(v.data[v.position+1]) << 8 v.position += 2 return result } @@ -49,7 +50,10 @@ func (v *StreamReader) GetWord() uint16 { // GetSWord returns a int16 word from the stream func (v *StreamReader) GetSWord() int16 { var result int16 - binary.Read(bytes.NewReader([]byte{v.data[v.position], v.data[v.position+1]}), binary.LittleEndian, &result) + err := binary.Read(bytes.NewReader([]byte{v.data[v.position], v.data[v.position+1]}), binary.LittleEndian, &result) + if err != nil { + log.Panic(err) + } v.position += 2 return result } @@ -57,9 +61,9 @@ func (v *StreamReader) GetSWord() int16 { // 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) + result += uint32(v.data[v.position+1]) << 8 + result += uint32(v.data[v.position+2]) << 16 + result += uint32(v.data[v.position+3]) << 24 v.position += 4 return result } diff --git a/Core/Engine.go b/Core/Engine.go index 6952287d..a2f4d62b 100644 --- a/Core/Engine.go +++ b/Core/Engine.go @@ -30,6 +30,8 @@ type EngineConfig struct { VsyncEnabled bool MpqPath string MpqLoadOrder []string + SfxVolume float64 + BgmVolume float64 } // Engine is the core OpenDiablo2 engine @@ -59,7 +61,8 @@ func CreateEngine() *Engine { result.loadPalettes() result.loadSoundEntries() result.SoundManager = Sound.CreateManager(result) - result.UIManager = UI.CreateManager(result) + result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume) + result.UIManager = UI.CreateManager(result, *result.SoundManager) result.LoadingSprite = result.LoadSprite(ResourcePaths.LoadingScreen, Palettes.Loading) loadingSpriteSizeX, loadingSpriteSizeY := result.LoadingSprite.GetSize() result.LoadingSprite.MoveTo(int(400-(loadingSpriteSizeX/2)), int(300+(loadingSpriteSizeY/2))) @@ -75,7 +78,10 @@ func (v *Engine) loadConfigurationFile() { } var config EngineConfig - json.Unmarshal(configJSON, &config) + err = json.Unmarshal(configJSON, &config) + if err != nil { + log.Fatal(err) + } v.Settings = config } diff --git a/MPQ/MPQ.go b/MPQ/MPQ.go index 996f2e65..bee3cd88 100644 --- a/MPQ/MPQ.go +++ b/MPQ/MPQ.go @@ -43,26 +43,26 @@ type HashTableEntry struct { // 16 bytes type FileFlag uint32 const ( - // MpqFileImplode - File is compressed using PKWARE Data compression library - MpqFileImplode FileFlag = 0x00000100 - // MpqFileCompress - File is compressed using combination of compression methods - MpqFileCompress FileFlag = 0x00000200 - // MpqFileEncrypted - The file is encrypted - MpqFileEncrypted FileFlag = 0x00010000 - // MpqFileFixKey - The decryption key for the file is altered according to the position of the file in the archive - MpqFileFixKey FileFlag = 0x00020000 - // MpqFilePatchFile - The file contains incremental patch for an existing file in base MPQ - MpqFilePatchFile FileFlag = 0x00100000 - // MpqFileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit - MpqFileSingleUnit FileFlag = 0x01000000 + // FileImplode - File is compressed using PKWARE Data compression library + FileImplode FileFlag = 0x00000100 + // FileCompress - File is compressed using combination of compression methods + FileCompress FileFlag = 0x00000200 + // FileEncrypted - The file is encrypted + FileEncrypted FileFlag = 0x00010000 + // FileFixKey - The decryption key for the file is altered according to the position of the file in the archive + FileFixKey FileFlag = 0x00020000 + // FilePatchFile - The file contains incremental patch for an existing file in base MPQ + FilePatchFile FileFlag = 0x00100000 + // FileSingleUnit - Instead of being divided to 0x1000-bytes blocks, the file is stored as single unit + FileSingleUnit FileFlag = 0x01000000 // FileDeleteMarker - File is a deletion marker, indicating that the file no longer exists. This is used to allow patch // archives to delete files present in lower-priority archives in the search chain. The file usually // has length of 0 or 1 byte and its name is a hash FileDeleteMarker FileFlag = 0x02000000 - // FileSEctorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded. - FileSEctorCrc FileFlag = 0x04000000 - // MpqFileExists - Set if file exists, reset when the file was deleted - MpqFileExists FileFlag = 0x80000000 + // FileSectorCrc - File has checksums for each sector. Ignored if file is not compressed or imploded. + FileSectorCrc FileFlag = 0x04000000 + // FileExists - Set if file exists, reset when the file was deleted + FileExists FileFlag = 0x80000000 ) // BlockTableEntry represents an entry in the block table @@ -111,9 +111,15 @@ func (v *MPQ) readHeader() error { } func (v *MPQ) loadHashTable() { - v.File.Seek(int64(v.Data.HashTableOffset), 0) + _, err := v.File.Seek(int64(v.Data.HashTableOffset), 0) + if err != nil { + log.Panic(err) + } hashData := make([]uint32, v.Data.HashTableEntries*4) - binary.Read(v.File, binary.LittleEndian, &hashData) + err = binary.Read(v.File, binary.LittleEndian, &hashData) + if err != nil { + log.Panic(err) + } decrypt(hashData, hashString("(hash table)", 3)) for i := uint32(0); i < v.Data.HashTableEntries; i++ { v.HashTableEntries = append(v.HashTableEntries, HashTableEntry{ @@ -128,9 +134,15 @@ func (v *MPQ) loadHashTable() { } func (v *MPQ) loadBlockTable() { - v.File.Seek(int64(v.Data.BlockTableOffset), 0) + _, err := v.File.Seek(int64(v.Data.BlockTableOffset), 0) + if err != nil { + log.Panic(err) + } blockData := make([]uint32, v.Data.BlockTableEntries*4) - binary.Read(v.File, binary.LittleEndian, &blockData) + err = binary.Read(v.File, binary.LittleEndian, &blockData) + if err != nil { + log.Panic(err) + } decrypt(blockData, hashString("(block table)", 3)) for i := uint32(0); i < v.Data.BlockTableEntries; i++ { v.BlockTableEntries = append(v.BlockTableEntries, BlockTableEntry{ @@ -208,9 +220,12 @@ func (v MPQ) GetFileBlockData(fileName string) (BlockTableEntry, error) { return v.BlockTableEntries[fileEntry.BlockIndex], nil } -// Close closses the MPQ file +// Close closes the MPQ file func (v *MPQ) Close() { - v.File.Close() + err := v.File.Close() + if err != nil { + log.Panic(err) + } } // ReadFile reads a file from the MPQ and returns a memory stream @@ -239,7 +254,7 @@ func (v MPQ) ReadTextFile(fileName string) (string, error) { func (v *BlockTableEntry) calculateEncryptionSeed() { fileName := path.Base(v.FileName) v.EncryptionSeed = hashString(fileName, 3) - if !v.HasFlag(MpqFileFixKey) { + if !v.HasFlag(FileFixKey) { return } v.EncryptionSeed = (v.EncryptionSeed + v.FilePosition) ^ v.UncompressedFileSize diff --git a/MPQ/MPQStream.go b/MPQ/MPQStream.go index 52c8d9cf..706c4568 100644 --- a/MPQ/MPQStream.go +++ b/MPQ/MPQStream.go @@ -5,6 +5,7 @@ import ( "compress/zlib" "encoding/binary" "fmt" + "log" "strings" "github.com/JoshVarga/blast" @@ -34,12 +35,12 @@ func CreateStream(mpq MPQ, blockTableEntry BlockTableEntry, fileName string) *St } fileSegs := strings.Split(fileName, `\`) result.EncryptionSeed = hashString(fileSegs[len(fileSegs)-1], 3) - if result.BlockTableEntry.HasFlag(MpqFileFixKey) { + if result.BlockTableEntry.HasFlag(FileFixKey) { result.EncryptionSeed = (result.EncryptionSeed + result.BlockTableEntry.FilePosition) ^ result.BlockTableEntry.UncompressedFileSize } result.BlockSize = 0x200 << result.MPQData.Data.BlockSize - if (result.BlockTableEntry.HasFlag(MpqFileCompress) || result.BlockTableEntry.HasFlag(MpqFileImplode)) && !result.BlockTableEntry.HasFlag(MpqFileSingleUnit) { + if (result.BlockTableEntry.HasFlag(FileCompress) || result.BlockTableEntry.HasFlag(FileImplode)) && !result.BlockTableEntry.HasFlag(FileSingleUnit) { result.loadBlockOffsets() } return result @@ -51,7 +52,7 @@ func (v *Stream) loadBlockOffsets() { v.MPQData.File.Seek(int64(v.BlockTableEntry.FilePosition), 0) binary.Read(v.MPQData.File, binary.LittleEndian, &v.BlockPositions) blockPosSize := blockPositionCount << 2 - if v.BlockTableEntry.HasFlag(MpqFileEncrypted) { + if v.BlockTableEntry.HasFlag(FileEncrypted) { decrypt(v.BlockPositions, v.EncryptionSeed-1) if v.BlockPositions[0] != blockPosSize { panic("Decryption of MPQ failed!") @@ -63,7 +64,7 @@ func (v *Stream) loadBlockOffsets() { } func (v *Stream) Read(buffer []byte, offset, count uint32) uint32 { - if v.BlockTableEntry.HasFlag(MpqFileSingleUnit) { + if v.BlockTableEntry.HasFlag(FileSingleUnit) { return v.readInternalSingleUnit(buffer, offset, count) } toRead := count @@ -94,13 +95,13 @@ func (v *Stream) readInternalSingleUnit(buffer []byte, offset, count uint32) uin func (v *Stream) readInternal(buffer []byte, offset, count uint32) uint32 { v.bufferData() localPosition := v.CurrentPosition % v.BlockSize - bytesToCopy := Common.Min(uint32(len(v.CurrentData))-localPosition, count) + bytesToCopy := Common.MinInt32(int32(len(v.CurrentData))-int32(localPosition), int32(count)) if bytesToCopy <= 0 { return 0 } - copy(buffer[offset:offset+bytesToCopy], v.CurrentData[localPosition:localPosition+bytesToCopy]) - v.CurrentPosition += bytesToCopy - return bytesToCopy + copy(buffer[offset:offset+uint32(bytesToCopy)], v.CurrentData[localPosition:localPosition+uint32(bytesToCopy)]) + v.CurrentPosition += uint32(bytesToCopy) + return uint32(bytesToCopy) } func (v *Stream) bufferData() { @@ -129,7 +130,7 @@ func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte { offset uint32 toRead uint32 ) - if v.BlockTableEntry.HasFlag(MpqFileCompress) || v.BlockTableEntry.HasFlag(MpqFileImplode) { + if v.BlockTableEntry.HasFlag(FileCompress) || v.BlockTableEntry.HasFlag(FileImplode) { offset = v.BlockPositions[blockIndex] toRead = v.BlockPositions[blockIndex+1] - offset } else { @@ -140,21 +141,21 @@ func (v *Stream) loadBlock(blockIndex, expectedLength uint32) []byte { data := make([]byte, toRead) v.MPQData.File.Seek(int64(offset), 0) binary.Read(v.MPQData.File, binary.LittleEndian, &data) - if v.BlockTableEntry.HasFlag(MpqFileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 { + if v.BlockTableEntry.HasFlag(FileEncrypted) && v.BlockTableEntry.UncompressedFileSize > 3 { if v.EncryptionSeed == 0 { panic("Unable to determine encryption key") } decryptBytes(data, blockIndex+v.EncryptionSeed) } - if v.BlockTableEntry.HasFlag(MpqFileCompress) && (toRead != expectedLength) { - if !v.BlockTableEntry.HasFlag(MpqFileSingleUnit) { + if v.BlockTableEntry.HasFlag(FileCompress) && (toRead != expectedLength) { + if !v.BlockTableEntry.HasFlag(FileSingleUnit) { data = decompressMulti(data, expectedLength) } else { data = pkDecompress(data) } } - if v.BlockTableEntry.HasFlag(MpqFileImplode) && (toRead != expectedLength) { + if v.BlockTableEntry.HasFlag(FileImplode) && (toRead != expectedLength) { data = pkDecompress(data) } @@ -215,23 +216,35 @@ func decompressMulti(data []byte, expectedLength uint32) []byte { func deflate(data []byte) []byte { b := bytes.NewReader(data) r, err := zlib.NewReader(b) - defer r.Close() if err != nil { panic(err) } buffer := new(bytes.Buffer) - buffer.ReadFrom(r) + _, err = buffer.ReadFrom(r) + if err != nil { + log.Panic(err) + } + err = r.Close() + if err != nil { + log.Panic(err) + } return buffer.Bytes() } func pkDecompress(data []byte) []byte { b := bytes.NewReader(data) r, err := blast.NewReader(b) - defer r.Close() if err != nil { panic(err) } buffer := new(bytes.Buffer) - buffer.ReadFrom(r) + _, err = buffer.ReadFrom(r) + if err != nil { + panic(err) + } + err = r.Close() + if err != nil { + panic(err) + } return buffer.Bytes() } diff --git a/Scenes/MainMenu.go b/Scenes/MainMenu.go index fd181c5a..e3d0563b 100644 --- a/Scenes/MainMenu.go +++ b/Scenes/MainMenu.go @@ -37,6 +37,7 @@ type MainMenu struct { copyrightLabel *UI.Label copyrightLabel2 *UI.Label openDiabloLabel *UI.Label + ShowTrademarkScreen bool leftButtonHeld bool } diff --git a/Sound/AudioProvider.go b/Sound/AudioProvider.go index 458d6ffb..e2ae62d8 100644 --- a/Sound/AudioProvider.go +++ b/Sound/AudioProvider.go @@ -14,6 +14,8 @@ type Manager struct { audioContext *audio.Context // The Audio context bgmAudio *audio.Player // The audio player lastBgm string + sfxVolume float64 + bgmVolume float64 } // CreateManager creates a sound provider @@ -21,7 +23,7 @@ func CreateManager(fileProvider Common.FileProvider) *Manager { result := &Manager{ fileProvider: fileProvider, } - audioContext, err := audio.NewContext(22050) + audioContext, err := audio.NewContext(44100) if err != nil { log.Fatal(err) } @@ -37,7 +39,10 @@ func (v *Manager) PlayBGM(song string) { v.lastBgm = song go func() { if v.bgmAudio != nil { - v.bgmAudio.Close() + err := v.bgmAudio.Close() + if err != nil { + log.Panic(err) + } } audioData := v.fileProvider.LoadFile(song) d, err := wav.Decode(v.audioContext, audio.BytesReadSeekCloser(audioData)) @@ -45,13 +50,29 @@ func (v *Manager) PlayBGM(song string) { log.Fatal(err) } s := audio.NewInfiniteLoop(d, int64(len(audioData))) - v.bgmAudio, err = audio.NewPlayer(v.audioContext, s) if err != nil { log.Fatal(err) } + v.bgmAudio.SetVolume(v.bgmVolume) // Play the infinite-length stream. This never ends. - v.bgmAudio.Rewind() - v.bgmAudio.Play() + err = v.bgmAudio.Rewind() + if err != nil { + panic(err) + } + err = v.bgmAudio.Play() + if err != nil { + panic(err) + } }() } + +func (v *Manager) LoadSoundEffect(sfx string) *SoundEffect { + result := CreateSoundEffect(sfx, v.fileProvider, v.audioContext, v.sfxVolume) + return result +} + +func (v *Manager) SetVolumes(bgmVolume, sfxVolume float64) { + v.sfxVolume = sfxVolume + v.bgmVolume = bgmVolume +} diff --git a/Sound/SoundEffect.go b/Sound/SoundEffect.go new file mode 100644 index 00000000..ce340c65 --- /dev/null +++ b/Sound/SoundEffect.go @@ -0,0 +1,38 @@ +package Sound + +import ( + "log" + + "github.com/hajimehoshi/ebiten/audio/wav" + + "github.com/essial/OpenDiablo2/Common" + + "github.com/hajimehoshi/ebiten/audio" +) + +type SoundEffect struct { + player *audio.Player +} + +func CreateSoundEffect(sfx string, fileProvider Common.FileProvider, context *audio.Context, volume float64) *SoundEffect { + result := &SoundEffect{} + + audioData := fileProvider.LoadFile(sfx) + d, err := wav.Decode(context, audio.BytesReadSeekCloser(audioData)) + if err != nil { + log.Fatal(err) + } + + player, err := audio.NewPlayer(context, d) + if err != nil { + log.Fatal(err) + } + player.SetVolume(volume) + result.player = player + return result +} + +func (v *SoundEffect) Play() { + v.player.Rewind() + v.player.Play() +} diff --git a/UI/Manager.go b/UI/Manager.go index af834be4..6782d43c 100644 --- a/UI/Manager.go +++ b/UI/Manager.go @@ -4,6 +4,7 @@ import ( "github.com/essial/OpenDiablo2/Common" "github.com/essial/OpenDiablo2/Palettes" "github.com/essial/OpenDiablo2/ResourcePaths" + "github.com/essial/OpenDiablo2/Sound" "github.com/hajimehoshi/ebiten" ) @@ -25,14 +26,16 @@ type Manager struct { pressedIndex int CursorX int CursorY int + clickSfx *Sound.SoundEffect } // CreateManager creates a new instance of a UI manager -func CreateManager(provider Common.FileProvider) *Manager { +func CreateManager(fileProvider Common.FileProvider, soundManager Sound.Manager) *Manager { result := &Manager{ pressedIndex: -1, widgets: make([]Widget, 0), - cursorSprite: provider.LoadSprite(ResourcePaths.CursorDefault, Palettes.Units), + cursorSprite: fileProvider.LoadSprite(ResourcePaths.CursorDefault, Palettes.Units), + clickSfx: soundManager.LoadSoundEffect(ResourcePaths.SFXButtonClick), } return result } @@ -85,6 +88,7 @@ func (v *Manager) Update() { if v.pressedIndex == -1 { found = true v.pressedIndex = i + v.clickSfx.Play() } else if v.pressedIndex > -1 && v.pressedIndex != i { v.widgets[i].SetPressed(false) } else { diff --git a/cibuild.sh b/cibuild.sh deleted file mode 100644 index 1080d443..00000000 --- a/cibuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -go get -golangci-lint run . -go build ./cmd/Client diff --git a/config.json b/config.json index b9a9b946..2a39dedf 100644 --- a/config.json +++ b/config.json @@ -4,6 +4,8 @@ "TicksPerSecond": 60, "RunInBackground": true, "VsyncEnabled": true, + "SfxVolume": 1.0, + "BgmVolume": 0.3, "MpqPath": "C:/Program Files (x86)/Diablo II", "MpqLoadOrder": [ "d2exp.mpq",