package item import ( "fmt" "log" "os" "path/filepath" "sync" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" ) type ItemStore struct { mu sync.Mutex dataDir string pathIndex map[string]string cache map[string]*ItemDef } func NewItemStore(dataDir string) *ItemStore { s := &ItemStore{ dataDir: dataDir, pathIndex: behavior.BuildPathIndex(filepath.Join(dataDir, "items")), cache: make(map[string]*ItemDef), } return s } func (s *ItemStore) Load(id string) (*ItemDef, error) { s.mu.Lock() defer s.mu.Unlock() if def, ok := s.cache[id]; ok { return def, nil } path, ok := s.pathIndex[id] if !ok { return nil, fmt.Errorf("read item %s: no such item", id) } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read item %s: %w", id, err) } var def ItemDef if err := yaml.Unmarshal(data, &def); err != nil { return nil, fmt.Errorf("parse item %s: %w", id, err) } warnDeclaredIDMismatch("item", path, id, def.ID) def.ID = id s.cache[id] = &def return &def, nil } func (s *ItemStore) LoadAll() ([]*ItemDef, error) { s.mu.Lock() defer s.mu.Unlock() dir := filepath.Join(s.dataDir, "items") var defs []*ItemDef err := behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { if def, ok := s.cache[id]; ok { defs = append(defs, def) return nil } var def ItemDef if err := yaml.Unmarshal(data, &def); err != nil { return fmt.Errorf("parse item %s: %w", id, err) } warnDeclaredIDMismatch("item", path, id, def.ID) def.ID = id s.cache[id] = &def defs = append(defs, &def) return nil }) return defs, err } // warnDeclaredIDMismatch logs when a data file declares an id: that disagrees // with its filename. IDs come from the filename (def.ID = id above), so the // declared value is silently ignored at runtime; without this warning an author // sees no sign that the id they wrote is dead. func warnDeclaredIDMismatch(kind, path, filenameID, declaredID string) { if declaredID != "" && declaredID != filenameID { log.Printf("%s %s: file declares id %q but is loaded as %q; the filename is authoritative and the declared id is ignored", kind, path, declaredID, filenameID) } } // PathIndex returns a copy of the id→path index, safe to iterate concurrently. func (s *ItemStore) PathIndex() map[string]string { s.mu.Lock() defer s.mu.Unlock() out := make(map[string]string, len(s.pathIndex)) for id, p := range s.pathIndex { out[id] = p } return out } func (s *ItemStore) IDSet() map[string]bool { s.mu.Lock() defer s.mu.Unlock() ids := make(map[string]bool, len(s.pathIndex)) for id := range s.pathIndex { ids[id] = true } return ids } func (s *ItemStore) Reload(dataDir string) { s.mu.Lock() defer s.mu.Unlock() s.dataDir = dataDir s.cache = make(map[string]*ItemDef) s.pathIndex = behavior.BuildPathIndex(filepath.Join(dataDir, "items")) }