mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-01-12 04:17:25 -05: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:
parent
46a60398d4
commit
07b0724575
@ -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
405
Common/Missiles.go
Normal 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
|
||||
}
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
BIN
OpenDiablo2.exe
BIN
OpenDiablo2.exe
Binary file not shown.
@ -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"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user