mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-11-18 02:16:23 -05:00
461 lines
13 KiB
Go
461 lines
13 KiB
Go
package d2records
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
|
)
|
|
|
|
const (
|
|
logPrefix = "Record Manager"
|
|
)
|
|
|
|
// NewRecordManager creates a new record manager (no loaders are bound!)
|
|
func NewRecordManager(l d2util.LogLevel) (*RecordManager, error) {
|
|
rm := &RecordManager{
|
|
boundLoaders: make(map[string][]recordLoader),
|
|
}
|
|
|
|
rm.Logger = d2util.NewLogger()
|
|
rm.Logger.SetPrefix(logPrefix)
|
|
rm.Logger.SetLevel(l)
|
|
|
|
err := rm.init()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rm, nil
|
|
}
|
|
|
|
// RecordManager stores all of the records loaded from txt files
|
|
type RecordManager struct {
|
|
*d2util.Logger
|
|
boundLoaders map[string][]recordLoader // there can be more than one loader bound for a file
|
|
Animation struct {
|
|
Data d2data.AnimationData
|
|
Token struct {
|
|
Player PlayerTypes
|
|
Composite CompositeTypes
|
|
Armor ArmorTypes
|
|
Weapon WeaponClasses
|
|
HitClass HitClasses
|
|
}
|
|
}
|
|
BodyLocations
|
|
Calculation struct {
|
|
Skills Calculations
|
|
Missiles Calculations
|
|
}
|
|
Character struct {
|
|
Classes PlayerClasses
|
|
Events
|
|
Experience ExperienceBreakpoints
|
|
MaxLevel ExperienceMaxLevels
|
|
Modes PlayerModes
|
|
Stats CharStats
|
|
}
|
|
ComponentCodes
|
|
Colors
|
|
DifficultyLevels
|
|
ElemTypes
|
|
Gamble
|
|
Hireling struct {
|
|
Details Hirelings
|
|
Descriptions HirelingDescriptions
|
|
}
|
|
Item struct {
|
|
All CommonItems // NOTE: populated when armor, weapons, and misc items are ALL loaded
|
|
|
|
Armors CommonItems
|
|
Misc CommonItems
|
|
Weapons CommonItems
|
|
|
|
Equivalency ItemEquivalenceMap // NOTE: populated when all items are loaded
|
|
EquivalenceByRecord ItemEquivalenceByRecord // NOTE: populated when all items are loaded
|
|
|
|
AutoMagic
|
|
Belts
|
|
Books
|
|
Gems
|
|
Magic struct {
|
|
Prefix MagicPrefix
|
|
Suffix MagicSuffix
|
|
}
|
|
MagicPrefixGroups ItemAffixGroups
|
|
MagicSuffixGroups ItemAffixGroups
|
|
Quality ItemQualities
|
|
LowQualityPrefixes LowQualities
|
|
Rare struct {
|
|
Prefix RarePrefixes
|
|
Suffix RareSuffixes
|
|
}
|
|
Ratios ItemRatios
|
|
Cube struct {
|
|
Recipes CubeRecipes
|
|
Modifiers CubeModifiers
|
|
Types CubeTypes
|
|
}
|
|
Runewords
|
|
Sets
|
|
SetItems
|
|
Stats ItemStatCosts
|
|
Treasure struct {
|
|
Normal TreasureClass
|
|
Expansion TreasureClass
|
|
}
|
|
Types ItemTypes
|
|
Unique UniqueItems
|
|
StorePages
|
|
}
|
|
Layout struct {
|
|
Inventory
|
|
Overlays
|
|
}
|
|
Level struct {
|
|
AutoMaps
|
|
Details LevelDetails
|
|
Maze LevelMazeDetails
|
|
Presets LevelPresets
|
|
Sub LevelSubstitutions
|
|
Types LevelTypes
|
|
Warp LevelWarps
|
|
}
|
|
Missiles
|
|
missilesByName
|
|
Monster struct {
|
|
AI MonsterAI
|
|
Equipment MonsterEquipment
|
|
Levels MonsterLevels
|
|
Modes MonModes
|
|
Name struct {
|
|
Prefix UniqueMonsterAffixes
|
|
Suffix UniqueMonsterAffixes
|
|
}
|
|
Placements MonsterPlacements
|
|
Presets MonPresets
|
|
Props MonsterProperties
|
|
Sequences MonsterSequences
|
|
Sounds MonsterSounds
|
|
Stats MonStats
|
|
Stats2 MonStats2
|
|
Types MonsterTypes
|
|
Unique struct {
|
|
Appellations UniqueAppellations
|
|
Mods MonsterUniqueModifiers
|
|
Constants MonsterUniqueModifierConstants
|
|
Super SuperUniques
|
|
}
|
|
}
|
|
NPCs
|
|
Object struct {
|
|
Details ObjectDetails
|
|
Lookup IndexedObjects
|
|
Modes ObjectModes
|
|
Shrines
|
|
Types ObjectTypes
|
|
}
|
|
PetTypes
|
|
Properties
|
|
Skill struct {
|
|
Details SkillDetails
|
|
Descriptions SkillDescriptions
|
|
}
|
|
Sound struct {
|
|
Details SoundDetails
|
|
Environment SoundEnvironments
|
|
}
|
|
States
|
|
}
|
|
|
|
func (r *RecordManager) init() error { // nolint:funlen // can't reduce
|
|
loaders := []struct {
|
|
path string
|
|
loader recordLoader
|
|
}{
|
|
{d2resource.LevelType, levelTypesLoader},
|
|
{d2resource.LevelPreset, levelPresetLoader},
|
|
{d2resource.LevelWarp, levelWarpsLoader},
|
|
{d2resource.ObjectType, objectTypesLoader},
|
|
{d2resource.ObjectDetails, objectDetailsLoader},
|
|
{d2resource.ObjectMode, objectModesLoader},
|
|
{d2resource.Weapons, weaponsLoader},
|
|
{d2resource.Armor, armorLoader},
|
|
{d2resource.Misc, miscItemsLoader},
|
|
{d2resource.Books, booksLoader},
|
|
{d2resource.Belts, beltsLoader},
|
|
{d2resource.Colors, colorsLoader},
|
|
{d2resource.ItemTypes, itemTypesLoader}, // WARN: needs to be after weapons, armor, and misc
|
|
{d2resource.UniqueItems, uniqueItemsLoader},
|
|
{d2resource.Missiles, missilesLoader},
|
|
{d2resource.SoundSettings, soundDetailsLoader},
|
|
{d2resource.MonStats, monsterStatsLoader},
|
|
{d2resource.MonStats2, monsterStats2Loader},
|
|
{d2resource.MonPreset, monsterPresetLoader},
|
|
{d2resource.MonProp, monsterPropertiesLoader},
|
|
{d2resource.MonType, monsterTypesLoader},
|
|
{d2resource.MonMode, monsterModeLoader},
|
|
{d2resource.MagicPrefix, magicPrefixLoader},
|
|
{d2resource.MagicSuffix, magicSuffixLoader},
|
|
{d2resource.ItemStatCost, itemStatCostLoader},
|
|
{d2resource.ItemRatio, itemRatioLoader},
|
|
{d2resource.StorePage, storePagesLoader},
|
|
{d2resource.Overlays, overlaysLoader},
|
|
{d2resource.CharStats, charStatsLoader},
|
|
{d2resource.Gamble, gambleLoader},
|
|
{d2resource.Hireling, hirelingLoader},
|
|
{d2resource.Experience, experienceLoader},
|
|
{d2resource.Gems, gemsLoader},
|
|
{d2resource.QualityItems, itemQualityLoader},
|
|
{d2resource.Runes, runewordLoader},
|
|
{d2resource.DifficultyLevels, difficultyLevelsLoader},
|
|
{d2resource.AutoMap, autoMapLoader},
|
|
{d2resource.LevelDetails, levelDetailsLoader},
|
|
{d2resource.LevelMaze, levelMazeDetailsLoader},
|
|
{d2resource.LevelSubstitutions, levelSubstitutionsLoader},
|
|
{d2resource.CubeRecipes, cubeRecipeLoader},
|
|
{d2resource.SuperUniques, monsterSuperUniqeLoader},
|
|
{d2resource.Inventory, inventoryLoader},
|
|
{d2resource.Skills, skillDetailsLoader},
|
|
{d2resource.SkillCalc, skillCalcLoader},
|
|
{d2resource.MissileCalc, missileCalcLoader},
|
|
{d2resource.Properties, propertyLoader},
|
|
{d2resource.SkillDesc, skillDescriptionLoader},
|
|
{d2resource.BodyLocations, bodyLocationsLoader},
|
|
{d2resource.Sets, setLoader},
|
|
{d2resource.SetItems, setItemLoader},
|
|
{d2resource.AutoMagic, autoMagicLoader},
|
|
{d2resource.TreasureClass, treasureClassLoader},
|
|
{d2resource.TreasureClassEx, treasureClassExLoader},
|
|
{d2resource.States, statesLoader},
|
|
{d2resource.SoundEnvirons, soundEnvironmentLoader},
|
|
{d2resource.Shrines, shrineLoader},
|
|
{d2resource.ElemType, elemTypesLoader},
|
|
{d2resource.PlrMode, playerModesLoader},
|
|
{d2resource.PetType, petTypesLoader},
|
|
{d2resource.NPC, npcLoader},
|
|
{d2resource.MonsterUniqueModifier, monsterUniqModifiersLoader},
|
|
{d2resource.MonsterEquipment, monsterEquipmentLoader},
|
|
{d2resource.UniqueAppellation, uniqueAppellationsLoader},
|
|
{d2resource.MonsterLevel, monsterLevelsLoader},
|
|
{d2resource.MonsterSound, monsterSoundsLoader},
|
|
{d2resource.MonsterSequence, monsterSequencesLoader},
|
|
{d2resource.PlayerClass, playerClassLoader},
|
|
{d2resource.MonsterPlacement, monsterPlacementsLoader},
|
|
{d2resource.ObjectGroup, objectGroupsLoader},
|
|
{d2resource.CompCode, componentCodesLoader},
|
|
{d2resource.MonsterAI, monsterAiLoader},
|
|
{d2resource.RarePrefix, rareItemPrefixLoader},
|
|
{d2resource.RareSuffix, rareItemSuffixLoader},
|
|
{d2resource.Events, eventsLoader},
|
|
{d2resource.ArmorType, armorTypesLoader}, // anim mode tokens
|
|
{d2resource.WeaponClass, weaponClassesLoader}, // anim mode tokens
|
|
{d2resource.PlayerType, playerTypeLoader}, // anim mode tokens
|
|
{d2resource.Composite, compositeTypeLoader}, // anim mode tokens
|
|
{d2resource.HitClass, hitClassLoader}, // anim mode tokens
|
|
{d2resource.UniquePrefix, uniqueMonsterPrefixLoader},
|
|
{d2resource.UniqueSuffix, uniqueMonsterSuffixLoader},
|
|
{d2resource.CubeModifier, cubeModifierLoader},
|
|
{d2resource.CubeType, cubeTypeLoader},
|
|
{d2resource.HirelingDescription, hirelingDescriptionLoader},
|
|
{d2resource.LowQualityItems, lowQualityLoader},
|
|
}
|
|
|
|
for idx := range loaders {
|
|
err := r.AddLoader(loaders[idx].path, loaders[idx].loader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
r.initObjectRecords(objectLookups)
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddLoader associates a file path with a record loader
|
|
func (r *RecordManager) AddLoader(path string, loader recordLoader) error {
|
|
if _, found := r.boundLoaders[path]; !found {
|
|
r.boundLoaders[path] = make([]recordLoader, 0)
|
|
}
|
|
|
|
r.boundLoaders[path] = append(r.boundLoaders[path], loader)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Load will pass the dictionary to any bound loaders and populate the record entries
|
|
func (r *RecordManager) Load(path string, dict *d2txt.DataDictionary) error {
|
|
loaders, found := r.boundLoaders[path]
|
|
if !found {
|
|
return fmt.Errorf("no loader bound for `%s`", path)
|
|
}
|
|
|
|
for idx := range loaders {
|
|
err := loaders[idx](r, dict)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// as soon as Armor, Weapons, and Misc items are loaded, we merge into r.Item.All
|
|
if r.Item.All == nil && r.Item.Armors != nil && r.Item.Weapons != nil && r.Item.Misc != nil {
|
|
r.Item.All = make(CommonItems)
|
|
|
|
for code := range r.Item.Armors {
|
|
r.Item.All[code] = r.Item.Armors[code]
|
|
}
|
|
|
|
for code := range r.Item.Weapons {
|
|
r.Item.All[code] = r.Item.Weapons[code]
|
|
}
|
|
|
|
for code := range r.Item.Misc {
|
|
r.Item.All[code] = r.Item.Misc[code]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMaxLevelByHero returns the highest level attainable for a hero type
|
|
func (r *RecordManager) GetMaxLevelByHero(heroType d2enum.Hero) int {
|
|
return r.Character.MaxLevel[heroType]
|
|
}
|
|
|
|
// GetExperienceBreakpoint given a hero type and a level, returns the experience required for the level
|
|
func (r *RecordManager) GetExperienceBreakpoint(heroType d2enum.Hero, level int) int {
|
|
return r.Character.Experience[level].HeroBreakpoints[heroType]
|
|
}
|
|
|
|
// GetLevelDetails gets a LevelDetailRecord by the record Id
|
|
func (r *RecordManager) GetLevelDetails(id int) *LevelDetailRecord {
|
|
for i := 0; i < len(r.Level.Details); i++ {
|
|
if r.Level.Details[i].ID == id {
|
|
return r.Level.Details[i]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LevelPreset looks up a LevelPresetRecord by ID
|
|
func (r *RecordManager) LevelPreset(id int) LevelPresetRecord {
|
|
for i := 0; i < len(r.Level.Presets); i++ {
|
|
if r.Level.Presets[i].DefinitionID == id {
|
|
return r.Level.Presets[i]
|
|
}
|
|
}
|
|
|
|
panic("Unknown level preset")
|
|
}
|
|
|
|
// FindEquivalentTypesByItemCommonRecord returns itemtype codes that are equivalent
|
|
// to the given item common record
|
|
func (r *RecordManager) FindEquivalentTypesByItemCommonRecord(
|
|
icr *ItemCommonRecord,
|
|
) []string {
|
|
if r.Item.EquivalenceByRecord == nil {
|
|
r.Item.EquivalenceByRecord = make(map[*ItemCommonRecord][]string)
|
|
}
|
|
|
|
// the first lookup generates the lookup table entry, next time will just use the table
|
|
if r.Item.EquivalenceByRecord[icr] == nil {
|
|
r.Item.EquivalenceByRecord[icr] = make([]string, 0)
|
|
|
|
for code := range r.Item.Equivalency {
|
|
icrList := r.Item.Equivalency[code]
|
|
for idx := range icrList {
|
|
if icr == icrList[idx] {
|
|
r.Item.EquivalenceByRecord[icr] = append(r.Item.EquivalenceByRecord[icr], code)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return r.Item.EquivalenceByRecord[icr]
|
|
}
|
|
|
|
func (r *RecordManager) initObjectRecords(lookups []ObjectLookupRecord) {
|
|
// Allocating 6 to allow Acts 1-5 without requiring a -1 at every read.
|
|
records := make(IndexedObjects, 6)
|
|
|
|
for i := range lookups {
|
|
record := &lookups[i]
|
|
if records[record.Act] == nil {
|
|
// Likewise allocating 3 so a -1 isn't necessary.
|
|
records[record.Act] = make([][]*ObjectLookupRecord, 3)
|
|
}
|
|
|
|
if records[record.Act][record.Type] == nil {
|
|
// For simplicity, allocating with length 1000 then filling the values in by index.
|
|
// If ids in the dictionary ever surpass 1000, raise this number.
|
|
records[record.Act][record.Type] = make([]*ObjectLookupRecord, 1000)
|
|
}
|
|
|
|
records[record.Act][record.Type][record.Id] = record
|
|
}
|
|
|
|
r.Object.Lookup = records
|
|
}
|
|
|
|
// LookupObject looks up an object record
|
|
func (r *RecordManager) LookupObject(act, typ, id int) *ObjectLookupRecord {
|
|
object := r.lookupObject(act, typ, id)
|
|
if object == nil {
|
|
r.Fatalf("Failed to look up object Act: %d, Type: %d, ID: %d", act, typ, id)
|
|
}
|
|
|
|
return object
|
|
}
|
|
|
|
func (r *RecordManager) lookupObject(act, typ, id int) *ObjectLookupRecord {
|
|
if len(r.Object.Lookup) < act {
|
|
return nil
|
|
}
|
|
|
|
if len(r.Object.Lookup[act]) < typ {
|
|
return nil
|
|
}
|
|
|
|
if len(r.Object.Lookup[act][typ]) < id {
|
|
return nil
|
|
}
|
|
|
|
return r.Object.Lookup[act][typ][id]
|
|
}
|
|
|
|
// SelectSoundByIndex selects a sound by its ID
|
|
func (r *RecordManager) SelectSoundByIndex(index int) *SoundDetailRecord {
|
|
for idx := range r.Sound.Details {
|
|
if r.Sound.Details[idx].Index == index {
|
|
return r.Sound.Details[idx]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSkillByName returns the skill record for the given Skill name.
|
|
func (r *RecordManager) GetSkillByName(skillName string) *SkillRecord {
|
|
for idx := range r.Skill.Details {
|
|
if r.Skill.Details[idx].Skill == skillName {
|
|
return r.Skill.Details[idx]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMissileByName allows lookup of a MissileRecord by a given name. The name will be lowercased and stripped of whitespaces.
|
|
func (r *RecordManager) GetMissileByName(missileName string) *MissileRecord {
|
|
return r.missilesByName[sanitizeMissilesKey(missileName)]
|
|
}
|