diff --git a/Common/LevelPresets.go b/Common/LevelPresets.go index 8ebf48ff..34bd0b78 100644 --- a/Common/LevelPresets.go +++ b/Common/LevelPresets.go @@ -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)) +} \ No newline at end of file diff --git a/Common/Missiles.go b/Common/Missiles.go new file mode 100644 index 00000000..8043d353 --- /dev/null +++ b/Common/Missiles.go @@ -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 +} \ No newline at end of file diff --git a/Common/Objects.go b/Common/Objects.go index 1fa106db..0d4034fe 100644 --- a/Common/Objects.go +++ b/Common/Objects.go @@ -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)) diff --git a/Common/StringUtils.go b/Common/StringUtils.go index eb4fb9b5..2ee0f78c 100644 --- a/Common/StringUtils.go +++ b/Common/StringUtils.go @@ -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) } diff --git a/Core/Engine.go b/Core/Engine.go index d4f2698c..c1e48770 100644 --- a/Core/Engine.go +++ b/Core/Engine.go @@ -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() diff --git a/OpenDiablo2.exe b/OpenDiablo2.exe index f35549b4..c1048f55 100644 Binary files a/OpenDiablo2.exe and b/OpenDiablo2.exe differ diff --git a/ResourcePaths/ResourcePaths.go b/ResourcePaths/ResourcePaths.go index 5b6ebf4f..661ab663 100644 --- a/ResourcePaths/ResourcePaths.go +++ b/ResourcePaths/ResourcePaths.go @@ -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" )