Added volume settings and sfx support

This commit is contained in:
Tim Sarbin 2019-10-26 20:09:33 -04:00
parent ec4949bc1b
commit 227e7e8b29
13 changed files with 169 additions and 61 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ type MainMenu struct {
copyrightLabel *UI.Label
copyrightLabel2 *UI.Label
openDiabloLabel *UI.Label
ShowTrademarkScreen bool
leftButtonHeld bool
}

View File

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

38
Sound/SoundEffect.go Normal file
View File

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

View File

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

View File

@ -1,4 +0,0 @@
#!/bin/bash
go get
golangci-lint run .
go build ./cmd/Client

View File

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