1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-25 16:45:23 +00:00

Fix MPQ load, add Missiles.txt load, switch LvlPrest.bin to LvlPrest.txt, fix sound loading (#80)

* fix mpqs, add missiles.txt, fix lvlprest
* fix sound loading issue
This commit is contained in:
ndechiara 2019-11-03 22:03:22 -05:00 committed by Tim Sarbin
parent 46a60398d4
commit 07b0724575
7 changed files with 564 additions and 71 deletions

View File

@ -8,62 +8,82 @@ import (
)
type LevelPresetRecord struct {
DefinitionId int32
LevelId int32
Name string
DefinitionId int
LevelId int
Populate bool
Logicals bool
Outdoors bool
Animate bool
KillEdge bool
FillBlanks bool
SizeX int32
SizeY int32
SizeX int
SizeY int
AutoMap bool
Scan bool
Pops int32
PopPad int32
Pops int
PopPad int
FileCount int
Files [6]string
Dt1Mask uint32
Dt1Mask uint
Beta bool
Expansion bool
}
// CreateLevelPresetRecord parses a row from lvlprest.txt into a LevelPresetRecord
func createLevelPresetRecord(props []string) LevelPresetRecord {
i := -1
inc := func() int {
i++
return i
}
result := LevelPresetRecord{
Name: props[inc()],
DefinitionId: StringToInt(props[inc()]),
LevelId: StringToInt(props[inc()]),
Populate: StringToUint8(props[inc()]) == 1,
Logicals: StringToUint8(props[inc()]) == 1,
Outdoors: StringToUint8(props[inc()]) == 1,
Animate: StringToUint8(props[inc()]) == 1,
KillEdge: StringToUint8(props[inc()]) == 1,
FillBlanks: StringToUint8(props[inc()]) == 1,
SizeX: StringToInt(props[inc()]),
SizeY: StringToInt(props[inc()]),
AutoMap: StringToUint8(props[inc()]) == 1,
Scan: StringToUint8(props[inc()]) == 1,
Pops: StringToInt(props[inc()]),
PopPad: StringToInt(props[inc()]),
FileCount: StringToInt(props[inc()]),
Files: [6]string{
props[inc()],
props[inc()],
props[inc()],
props[inc()],
props[inc()],
props[inc()],
},
Dt1Mask: StringToUint(props[inc()]),
Beta: StringToUint8(props[inc()]) == 1,
Expansion: StringToUint8(props[inc()]) == 1,
}
return result
}
var LevelPresets map[int]*LevelPresetRecord
func LoadLevelPresets(fileProvider FileProvider) {
levelTypesData := fileProvider.LoadFile(ResourcePaths.LevelPreset)
sr := CreateStreamReader(levelTypesData)
sr.SkipBytes(4) // Count
LevelPresets = make(map[int]*LevelPresetRecord)
for !sr.Eof() {
i := int(sr.GetInt32())
LevelPresets[i] = &LevelPresetRecord{}
LevelPresets[i].DefinitionId = int32(i)
LevelPresets[i].LevelId = sr.GetInt32()
LevelPresets[i].Populate = sr.GetInt32() != 0
LevelPresets[i].Logicals = sr.GetInt32() != 0
LevelPresets[i].Outdoors = sr.GetInt32() != 0
LevelPresets[i].Animate = sr.GetInt32() != 0
LevelPresets[i].KillEdge = sr.GetInt32() != 0
LevelPresets[i].FillBlanks = sr.GetInt32() != 0
sr.GetInt32() // What is this field?
LevelPresets[i].SizeX = sr.GetInt32()
LevelPresets[i].SizeY = sr.GetInt32()
LevelPresets[i].AutoMap = sr.GetInt32() != 0
LevelPresets[i].Scan = sr.GetInt32() != 0
LevelPresets[i].Pops = sr.GetInt32()
LevelPresets[i].PopPad = sr.GetInt32()
sr.GetUInt32() // Most likely NumFiles
for fileIdx := 0; fileIdx < 6; fileIdx++ {
strData, _ := sr.ReadBytes(60)
s := strings.Trim(string(strData), string(0))
if s == "0" {
LevelPresets[i].Files[fileIdx] = ""
} else {
LevelPresets[i].Files[fileIdx] = s
}
data := strings.Split(string(fileProvider.LoadFile(ResourcePaths.LevelPreset)), "\r\n")[1:]
for _, line := range data {
if len(line) == 0 {
continue
}
LevelPresets[i].Dt1Mask = sr.GetUInt32()
props := strings.Split(line, "\t")
if(props[1] == "") {
continue // any line without a definition id is skipped (e.g. the "Expansion" line)
}
rec := createLevelPresetRecord(props)
LevelPresets[rec.DefinitionId] = &rec
}
log.Printf("Loaded %d LevelPreset records", len(LevelPresets))
}
log.Printf("Loaded %d level presets", len(LevelPresets))
}

405
Common/Missiles.go Normal file
View File

@ -0,0 +1,405 @@
package Common
import (
"log"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/ResourcePaths"
)
type MissileCalcParam struct {
Param int
Desc string
}
type MissileCalc struct {
Calc string
Desc string
Params []MissileCalcParam
}
type MissileLight struct {
Diameter int
Flicker int
Red uint8
Green uint8
Blue uint8
}
type MissileAnimation struct {
StepsBeforeVisible int
StepsBeforeActive int
LoopAnimation bool
CelFileName string
AnimationRate int // seems to do nothing
AnimationLength int
AnimationSpeed int
StartingFrame int // called "RandFrame"
HasSubLoop bool // runs after first animation ends
SubStartingFrame int
SubEndingFrame int
}
type MissileCollision struct {
CollisionType int // controls the kind of collision
// 0 = none, 1 = units only, 3 = normal (units, walls),
// 6 = walls only, 8 = walls, units, and floors
DestroyedUponCollision bool
FriendlyFire bool
LastCollide bool // unknown
Collision bool // unknown
ClientCollision bool // unknown
ClientSend bool // unclear
UseCollisionTimer bool // after hit, use timer before dying
TimerFrames int // how many frames to persist
}
type MissileDamage struct {
MinDamage int
MaxDamage int
MinLevelDamage [5]int // additional damage per missile level
// [0]: lvs 2-8, [1]: lvs 9-16, [2]: lvs 17-22, [3]: lvs 23-28, [4]: lv 29+
MaxLevelDamage [5]int // see above
DamageSynergyPerCalc string // works like synergy in skills.txt, not clear
}
type MissileElementalDamage struct {
Damage MissileDamage
ElementType string
Duration int // frames, 25 = 1 second
LevelDuration [3]int // 0,1,2, unknown level intervals, bonus duration per level
}
type MissileRecord struct {
Name string
Id int
ClientMovementFunc int
ClientCollisionFunc int
ServerMovementFunc int
ServerCollisionFunc int
ServerDamageFunc int
ServerMovementCalc MissileCalc
ClientMovementCalc MissileCalc
ServerCollisionCalc MissileCalc
ClientCollisionCalc MissileCalc
ServerDamageCalc MissileCalc
Velocity int
MaxVelocity int
LevelVelocityBonus int
Accel int
Range int
LevelRangeBonus int
Light MissileLight
Animation MissileAnimation
Collision MissileCollision
XOffset int
YOffset int
ZOffset int
Size int // diameter
DestroyedByTP bool // if true, destroyed when source player teleports to town
DestroyedByTPFrame int // see above, for client side, (this is a guess) which frame it vanishes on
CanDestroy bool // unknown
UseAttackRating bool // if true, uses 'attack rating' to determine if it hits or misses
// if false, has a 95% chance to hit.
AlwaysExplode bool // if true, always calls its collision function when it is destroyed, even if it doesn't hit anything
// note that some collision functions (lightning fury) seem to ignore this and always explode regardless of setting (requires investigation)
ClientExplosion bool // if true, does not really exist
// is only aesthetic / client side
TownSafe bool // if true, doesn't vanish when spawned in town
// if false, vanishes when spawned in town
IgnoreBossModifiers bool // if true, doesn't get bonuses from boss mods
IgnoreMultishot bool // if true, can't gain the mulitshot modifier
HolyFilterType int // specifies what this missile can hit
// 0 = all units, 1 = undead only, 2 = demons only, 3 = all units (again?)
CanBeSlowed bool // if true, is affected by skill_handofathena
TriggersHitEvents bool // if true, triggers events that happen "upon getting hit" on targets
TriggersGetHit bool // if true, can cause target to enter hit recovery mode
SoftHit bool // unknown
KnockbackPercent int // chance of knocking the target back, 0-100
TransparencyMode int // controls rendering
// 0 = normal, 1 = alpha blending (darker = more transparent)
// 2 = special (black and white?)
UseQuantity bool // if true, uses quantity
// not clear what this means. Also apparently requires a special starting function in skills.txt
AffectedByPierce bool // if true, affected by the pierce modifier and the Pierce skill
SpecialSetup bool // unknown, only true for potions
MissileSkill bool // if true, applies elemental damage from items to the splash radius instead of normal damage modifiers
SkillName string // if not empty, the missile will refer to this skill instead of its own data for the following:
// "ResultFlags, HitFlags, HitShift, HitClass, SrcDamage (SrcDam in skills.txt!),
// MinDam, MinLevDam1-5, MaxDam, MaxLevDam1-5, DmgSymPerCalc, EType, EMin, EMinLev1-5,
// EMax, EMaxLev1-5, EDmgSymPerCalc, ELen, ELenLev1-3, ELenSymPerCalc"
ResultFlags int // unknown
// 4 = normal missiles, 5 = explosions, 8 = non-damaging missiles
HitFlags int // unknown
// 2 = explosions, 5 = freezing arrow
HitShift int // damage is measured in 256s
// the actual damage is [damage] * 2 ^ [hitshift]
// e.g. 100 damage, 8 hitshift = 100 * 2 ^ 8 = 100 * 256 = 25600
// (visually, the damage is this result / 256)
ApplyMastery bool // unknown
SourceDamage int // 0-128, 128 is 100%
// percentage of source units attack properties to apply to the missile?
// not only affects damage but also other modifiers like lifesteal and manasteal (need a complete list)
// setting this to -1 "gets rid of SrcDmg from skills.txt", not clear what that means
HalfDamageForTwoHander bool // if true, damage is halved when a two-handed weapon is used
SourceMissDamage int // 0-128, 128 is 100%
// unknown, only used for poison clouds.
Damage MissileDamage
ElementalDamage MissileElementalDamage
HitClass int // controls clientside aesthetic effects for collisions
// particularly sound effects that are played on a hit
NumDirections int // count of dirs in the DCC loaded by CelFile
// apparently this value is no longer needed in D2
LocalBlood int // blood effects?
// 0 = no blood, 1 = blood, 2 = blood and affected by open wounds
DamageReductionRate int // how many frames between applications of the
// magic_damage_reduced stat, so for instance on a 0 this stat applies every frame
// on a 3, only every 4th frame has damage reduced
TravelSound string // name of sound to play during lifetime
// whether or not it loops depends on the specific sound's settings?
// if it doesn't loop, it's just a on-spawn sound effect
HitSound string // sound plays upon collision
ProgSound string // plays at "special events", like a mariachi band
ProgOverlay string // name of an overlay from overlays.txt to use at special events
ExplosionMissile string // name of a missile from missiles.txt that is created upon collision
// or anytime it is destroyed if AlwaysExplode is true
SubMissile [3]string // 0,1,2 name of missiles spawned by movement function
HitSubMissile [4]string // 0,1,2 name of missiles spawned by collision function
ClientSubMissile [3]string // see above, but for client only
ClientHitSubMissile [4]string // see above, but for client only
}
func createMissileRecord(line string) MissileRecord {
r := strings.Split(line, "\t")
i := -1
inc := func() int {
i++
return i
}
// note: in this file, empties are equivalent to zero, so all numerical conversions should
// be wrapped in an EmptyToZero transform
result := MissileRecord{
Name: r[inc()],
Id: StringToInt(EmptyToZero(r[inc()])),
ClientMovementFunc: StringToInt(EmptyToZero(AsterToEmpty(r[inc()]))),
ClientCollisionFunc: StringToInt(EmptyToZero(AsterToEmpty(r[inc()]))),
ServerMovementFunc: StringToInt(EmptyToZero(AsterToEmpty(r[inc()]))),
ServerCollisionFunc: StringToInt(EmptyToZero(AsterToEmpty(r[inc()]))),
ServerDamageFunc: StringToInt(EmptyToZero(AsterToEmpty(r[inc()]))),
ServerMovementCalc: loadMissileCalc(&r, inc, 5),
ClientMovementCalc: loadMissileCalc(&r, inc, 5),
ServerCollisionCalc: loadMissileCalc(&r, inc, 3),
ClientCollisionCalc: loadMissileCalc(&r, inc, 3),
ServerDamageCalc: loadMissileCalc(&r, inc, 2),
Velocity: StringToInt(EmptyToZero(r[inc()])),
MaxVelocity: StringToInt(EmptyToZero(r[inc()])),
LevelVelocityBonus: StringToInt(EmptyToZero(r[inc()])),
Accel: StringToInt(EmptyToZero(r[inc()])),
Range: StringToInt(EmptyToZero(r[inc()])),
LevelRangeBonus: StringToInt(EmptyToZero(r[inc()])),
Light: loadMissileLight(&r, inc),
Animation: loadMissileAnimation(&r, inc),
Collision: loadMissileCollision(&r, inc),
XOffset: StringToInt(EmptyToZero(r[inc()])),
YOffset: StringToInt(EmptyToZero(r[inc()])),
ZOffset: StringToInt(EmptyToZero(r[inc()])),
Size: StringToInt(EmptyToZero(r[inc()])),
DestroyedByTP: StringToInt(EmptyToZero(r[inc()])) == 1,
DestroyedByTPFrame: StringToInt(EmptyToZero(r[inc()])),
CanDestroy: StringToInt(EmptyToZero(r[inc()])) == 1,
UseAttackRating: StringToInt(EmptyToZero(r[inc()])) == 1,
AlwaysExplode: StringToInt(EmptyToZero(r[inc()])) == 1,
ClientExplosion: StringToInt(EmptyToZero(r[inc()])) == 1,
TownSafe: StringToInt(EmptyToZero(r[inc()])) == 1,
IgnoreBossModifiers: StringToInt(EmptyToZero(r[inc()])) == 1,
IgnoreMultishot: StringToInt(EmptyToZero(r[inc()])) == 1,
HolyFilterType: StringToInt(EmptyToZero(r[inc()])),
CanBeSlowed: StringToInt(EmptyToZero(r[inc()])) == 1,
TriggersHitEvents: StringToInt(EmptyToZero(r[inc()])) == 1,
TriggersGetHit: StringToInt(EmptyToZero(r[inc()])) == 1,
SoftHit: StringToInt(EmptyToZero(r[inc()])) == 1,
KnockbackPercent: StringToInt(EmptyToZero(r[inc()])),
TransparencyMode: StringToInt(EmptyToZero(r[inc()])),
UseQuantity: StringToInt(EmptyToZero(r[inc()])) == 1,
AffectedByPierce: StringToInt(EmptyToZero(r[inc()])) == 1,
SpecialSetup: StringToInt(EmptyToZero(r[inc()])) == 1,
MissileSkill: StringToInt(EmptyToZero(r[inc()])) == 1,
SkillName: r[inc()],
ResultFlags: StringToInt(EmptyToZero(r[inc()])),
HitFlags: StringToInt(EmptyToZero(r[inc()])),
HitShift: StringToInt(EmptyToZero(r[inc()])),
ApplyMastery: StringToInt(EmptyToZero(r[inc()])) == 1,
SourceDamage: StringToInt(EmptyToZero(r[inc()])),
HalfDamageForTwoHander: StringToInt(EmptyToZero(r[inc()])) == 1,
SourceMissDamage: StringToInt(EmptyToZero(r[inc()])),
Damage: loadMissileDamage(&r, inc),
ElementalDamage: loadMissileElementalDamage(&r, inc),
HitClass: StringToInt(EmptyToZero(r[inc()])),
NumDirections: StringToInt(EmptyToZero(r[inc()])),
LocalBlood: StringToInt(EmptyToZero(r[inc()])),
DamageReductionRate: StringToInt(EmptyToZero(r[inc()])),
TravelSound: r[inc()],
HitSound: r[inc()],
ProgSound: r[inc()],
ProgOverlay: r[inc()],
ExplosionMissile: r[inc()],
SubMissile: [3]string{r[inc()], r[inc()], r[inc()]},
HitSubMissile: [4]string{r[inc()], r[inc()], r[inc()], r[inc()]},
ClientSubMissile: [3]string{r[inc()], r[inc()], r[inc()]},
ClientHitSubMissile: [4]string{r[inc()], r[inc()], r[inc()], r[inc()]},
}
return result
}
var Missiles map[int]*MissileRecord
func LoadMissiles(fileProvider FileProvider) {
Missiles = make(map[int]*MissileRecord)
data := strings.Split(string(fileProvider.LoadFile(ResourcePaths.Missiles)), "\r\n")[1:]
for _, line := range data {
if len(line) == 0 {
continue
}
rec := createMissileRecord(line)
Missiles[rec.Id] = &rec
}
log.Printf("Loaded %d missiles", len(Missiles))
}
func loadMissileCalcParam(r *[]string, inc func() int) MissileCalcParam {
result := MissileCalcParam{
Param: StringToInt(EmptyToZero((*r)[inc()])),
Desc: (*r)[inc()],
}
return result
}
func loadMissileCalc(r *[]string, inc func() int, params int) MissileCalc {
result := MissileCalc{
Calc: (*r)[inc()],
Desc: (*r)[inc()],
}
result.Params = make([]MissileCalcParam, params)
for p := 0; p < params; p++ {
result.Params[p] = loadMissileCalcParam(r, inc);
}
return result
}
func loadMissileLight(r *[]string, inc func() int) MissileLight {
result := MissileLight{
Diameter: StringToInt(EmptyToZero((*r)[inc()])),
Flicker: StringToInt(EmptyToZero((*r)[inc()])),
Red: StringToUint8(EmptyToZero((*r)[inc()])),
Green: StringToUint8(EmptyToZero((*r)[inc()])),
Blue: StringToUint8(EmptyToZero((*r)[inc()])),
}
return result
}
func loadMissileAnimation(r *[]string, inc func() int) MissileAnimation {
result := MissileAnimation{
StepsBeforeVisible: StringToInt(EmptyToZero((*r)[inc()])),
StepsBeforeActive: StringToInt(EmptyToZero((*r)[inc()])),
LoopAnimation: StringToInt(EmptyToZero((*r)[inc()])) == 1,
CelFileName: (*r)[inc()],
AnimationRate: StringToInt(EmptyToZero((*r)[inc()])),
AnimationLength: StringToInt(EmptyToZero((*r)[inc()])),
AnimationSpeed: StringToInt(EmptyToZero((*r)[inc()])),
StartingFrame: StringToInt(EmptyToZero((*r)[inc()])),
HasSubLoop: StringToInt(EmptyToZero((*r)[inc()])) == 1,
SubStartingFrame: StringToInt(EmptyToZero((*r)[inc()])),
SubEndingFrame: StringToInt(EmptyToZero((*r)[inc()])),
}
return result
}
func loadMissileCollision(r *[]string, inc func() int) MissileCollision {
result := MissileCollision{
CollisionType: StringToInt(EmptyToZero((*r)[inc()])),
DestroyedUponCollision: StringToInt(EmptyToZero((*r)[inc()])) == 1,
FriendlyFire: StringToInt(EmptyToZero((*r)[inc()])) == 1,
LastCollide: StringToInt(EmptyToZero((*r)[inc()])) == 1,
Collision: StringToInt(EmptyToZero((*r)[inc()])) == 1,
ClientCollision: StringToInt(EmptyToZero((*r)[inc()])) == 1,
ClientSend: StringToInt(EmptyToZero((*r)[inc()])) == 1,
UseCollisionTimer: StringToInt(EmptyToZero((*r)[inc()])) == 1,
TimerFrames: StringToInt(EmptyToZero((*r)[inc()])),
}
return result
}
func loadMissileDamage(r *[]string, inc func() int) MissileDamage {
result := MissileDamage{
MinDamage: StringToInt(EmptyToZero((*r)[inc()])),
MinLevelDamage: [5]int{
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
},
MaxDamage: StringToInt(EmptyToZero((*r)[inc()])),
MaxLevelDamage: [5]int{
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
},
DamageSynergyPerCalc: (*r)[inc()],
}
return result
}
func loadMissileElementalDamage(r *[]string, inc func() int) MissileElementalDamage {
result := MissileElementalDamage{
ElementType: (*r)[inc()],
Damage: loadMissileDamage(r, inc),
Duration: StringToInt(EmptyToZero((*r)[inc()])),
LevelDuration: [3]int{
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
StringToInt(EmptyToZero((*r)[inc()])),
},
}
return result
}

View File

@ -120,8 +120,7 @@ type ObjectRecord struct {
}
// CreateObjectRecord parses a row from objects.txt into an object record
func createObjectRecord(line string) ObjectRecord {
props := strings.Split(line, "\t")
func createObjectRecord(props []string) ObjectRecord {
i := -1
inc := func() int {
i++
@ -343,7 +342,11 @@ func LoadObjects(fileProvider FileProvider) {
if len(line) == 0 {
continue
}
rec := createObjectRecord(line)
props := strings.Split(line, "\t")
if props[2] == "" {
continue // skip a line that doesn't have an id
}
rec := createObjectRecord(props)
Objects[rec.Id] = &rec
}
log.Printf("Loaded %d objects", len(Objects))

View File

@ -9,6 +9,22 @@ import (
"unicode/utf8"
)
// AsterToEmpty converts strings beginning with "*" to "", for use when handling columns where an asterix can be used to comment out entries
func AsterToEmpty(text string) string {
if strings.HasPrefix(text, "*") {
return ""
}
return text
}
// EmptyToZero converts empty strings to "0" and leaves non-empty strings as is, for use before converting numerical data which equates empty to zero
func EmptyToZero(text string) string {
if text == "" || text == " " {
return "0"
}
return text
}
// StringToInt converts a string to an integer
func StringToInt(text string) int {
result, err := strconv.Atoi(text)
@ -18,6 +34,15 @@ func StringToInt(text string) int {
return result
}
// StringToUint converts a string to a uint32
func StringToUint(text string) uint {
result, err := strconv.ParseUint(text, 10, 32)
if err != nil {
panic(err)
}
return uint(result)
}
// StringToUint8 converts a string to an uint8
func StringToUint8(text string) uint8 {
result, err := strconv.Atoi(text)
@ -25,7 +50,7 @@ func StringToUint8(text string) uint8 {
panic(err)
}
if result < 0 || result > 255 {
panic("value out of range of byte")
panic(fmt.Sprintf("value %d out of range of byte", result))
}
return uint8(result)
}
@ -37,7 +62,7 @@ func StringToInt8(text string) int8 {
panic(err)
}
if result < -128 || result > 122 {
panic("value out of range of a signed byte")
panic(fmt.Sprintf("value %d out of range of a signed byte", result))
}
return int8(result)
}

View File

@ -9,6 +9,7 @@ import (
"path"
"strings"
"sync"
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/PaletteDefs"
@ -43,6 +44,7 @@ type EngineConfig struct {
type Engine struct {
Settings *EngineConfig // Engine configuration settings from json file
Files map[string]*Common.MpqFileRecord // Map that defines which files are in which MPQs
CheckedPatch map[string]bool // First time we check a file, we'll check if it's in the patch. This notes that we've already checked that.
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
@ -69,6 +71,7 @@ func CreateEngine() *Engine {
Common.LoadLevelWarps(result)
Common.LoadObjectTypes(result)
Common.LoadObjects(result)
Common.LoadMissiles(result)
Common.LoadSounds(result)
result.SoundManager = Sound.CreateManager(result)
result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume)
@ -112,9 +115,11 @@ func (v *Engine) loadConfigurationFile() {
func (v *Engine) mapMpqFiles() {
log.Println("mapping mpq file structure")
v.Files = make(map[string]*Common.MpqFileRecord)
v.CheckedPatch = make(map[string]bool)
for _, mpqFileName := range v.Settings.MpqLoadOrder {
mpqPath := path.Join(v.Settings.MpqPath, mpqFileName)
mpq, err := MPQ.Load(mpqPath)
if err != nil {
log.Fatal(err)
}
@ -126,14 +131,16 @@ func (v *Engine) mapMpqFiles() {
fileList := strings.Split(string(fileListText), "\r\n")
for _, filePath := range fileList {
if _, exists := v.Files[strings.ToLower(filePath)]; exists {
if v.Files[strings.ToLower(filePath)].IsPatch {
v.Files[strings.ToLower(filePath)].UnpatchedMpqFile = mpqPath
transFilePath := `/`+strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)
if _, exists := v.Files[transFilePath]; exists {
if v.Files[transFilePath].IsPatch {
v.Files[transFilePath].UnpatchedMpqFile = mpqPath
}
continue
}
v.Files[`/`+strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)] = &Common.MpqFileRecord{
v.Files[transFilePath] = &Common.MpqFileRecord{
mpqPath, false, ""}
v.CheckedPatch[transFilePath] = false
}
}
}
@ -143,27 +150,61 @@ 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)
fileName = strings.ReplaceAll(fileName, `\`, `/`)
var mpqLookupFileName string
if strings.HasPrefix(fileName, "/") || strings.HasPrefix(fileName,"\\") {
mpqLookupFileName = strings.ReplaceAll(fileName, `/`, `\`)[1:]
} else {
mpqLookupFileName = strings.ReplaceAll(fileName, `/`, `\`)
}
mutex.Lock()
// TODO: May want to cache some things if performance becomes an issue
mpqFile := v.Files[strings.ToLower(fileName)]
var mpq MPQ.MPQ
var err error
if mpqFile == nil {
// always try to load from patch first
checked, checkok := v.CheckedPatch[strings.ToLower(fileName)]
patchLoaded := false
if !checked || !checkok {
patchMpqFilePath := path.Join(v.Settings.MpqPath, v.Settings.MpqLoadOrder[0])
mpq, err = MPQ.Load(patchMpqFilePath)
if err == nil {
// loaded patch mpq. check if this file exists in it
fileInPatch := mpq.FileExists(mpqLookupFileName)
if fileInPatch {
patchLoaded = true
// set the path to the patch so it will be loaded there in the future
mpqFile = &Common.MpqFileRecord{patchMpqFilePath, false, ""}
v.Files[strings.ToLower(fileName)] = mpqFile
}
}
v.CheckedPatch[strings.ToLower(fileName)] = true
}
if patchLoaded {
// if we already loaded the correct mpq from the patch check, don't bother reloading it
} else if mpqFile == nil {
// Super secret non-listed file?
found := false
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) {
if !mpq.FileExists(fileName) {
continue
}
// We found the super-secret file!
found = true
v.Files[strings.ToLower(fileName)] = &Common.MpqFileRecord{mpqFilePath, false, ""}
break
}
if !found {
log.Fatal(fmt.Sprintf("File '%s' not found during preload of listfiles, and could not be located in any MPQ checking manually.", fileName))
}
} else if mpqFile.IsPatch {
log.Fatal("Tried to load a patchfile")
} else {
@ -174,13 +215,12 @@ func (v *Engine) LoadFile(fileName string) []byte {
}
}
fileName = strings.ReplaceAll(fileName, `/`, `\`)[1:]
blockTableEntry, err := mpq.GetFileBlockData(fileName)
blockTableEntry, err := mpq.GetFileBlockData(mpqLookupFileName)
if err != nil {
log.Printf("Error locating block data entry for '%s' in mpq file '%s'", fileName, mpq.FileName)
log.Printf("Error locating block data entry for '%s' in mpq file '%s'", mpqLookupFileName, mpq.FileName)
log.Fatal(err)
}
mpqStream := MPQ.CreateStream(mpq, blockTableEntry, fileName)
mpqStream := MPQ.CreateStream(mpq, blockTableEntry, mpqLookupFileName)
result := make([]byte, blockTableEntry.UncompressedFileSize)
mpqStream.Read(result, 0, blockTableEntry.UncompressedFileSize)
mutex.Unlock()

Binary file not shown.

View File

@ -161,7 +161,7 @@ const (
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"
LevelPreset = "/data/global/excel/LvlPrest.txt"
LevelType = "/data/global/excel/LvlTypes.bin"
ObjectType = "/data/global/excel/objtype.bin"
LevelWarp = "/data/global/excel/LvlWarp.bin"
@ -228,21 +228,21 @@ const (
// --- Sound Effects ---
SFXButtonClick = "ESOUND_CURSOR_BUTTON_CLICK"
SFXAmazonDeselect = "ESOUND_CURSOR_AMAZON_DESELECT"
SFXAmazonSelect = "ESOUND_CURSOR_AMAZON_SELECT"
SFXButtonClick = "cursor_button_click"
SFXAmazonDeselect = "cursor_amazon_deselect"
SFXAmazonSelect = "cursor_amazon_select"
SFXAssassinDeselect = "/data/global/sfx/Cursor/intro/assassin deselect.wav"
SFXAssassinSelect = "/data/global/sfx/Cursor/intro/assassin select.wav"
SFXBarbarianDeselect = "ESOUND_CURSOR_BARBARIAN_DESELECT"
SFXBarbarianSelect = "ESOUND_CURSOR_BARBARIAN_SELECT"
SFXBarbarianDeselect = "cursor_barbarian_deselect"
SFXBarbarianSelect = "cursor_barbarian_select"
SFXDruidDeselect = "/data/global/sfx/Cursor/intro/druid deselect.wav"
SFXDruidSelect = "/data/global/sfx/Cursor/intro/druid 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"
SFXNecromancerDeselect = "cursor_necromancer_deselect"
SFXNecromancerSelect = "cursor_necromancer_select"
SFXPaladinDeselect = "cursor_paladin_deselect"
SFXPaladinSelect = "cursor_paladin_select"
SFXSorceressDeselect = "cursor_sorceress_deselect"
SFXSorceressSelect = "cursor_sorceress_select"
// --- Enemy Data ---
@ -250,5 +250,5 @@ const (
// --- Skill Data ---
Missiles = "/data//global//excel//missiles.txt"
Missiles = "/data/global/excel/Missiles.txt"
)