OpenDiablo2/d2core/d2records/record_manager.go

460 lines
13 KiB
Go

package d2records
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2animdata"
"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 *d2animdata.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)]
}