Files
clawgo/pkg/world/store.go
2026-03-16 12:04:55 +08:00

245 lines
5.1 KiB
Go

package world
import (
"bufio"
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
type Store struct {
dir string
worldPath string
npcPath string
eventsPath string
mu sync.RWMutex
}
func NewStore(workspace string) *Store {
workspace = strings.TrimSpace(workspace)
if workspace == "" {
return nil
}
dir := filepath.Join(workspace, "agents", "runtime")
_ = os.MkdirAll(dir, 0755)
return &Store{
dir: dir,
worldPath: filepath.Join(dir, "world_state.json"),
npcPath: filepath.Join(dir, "npc_state.json"),
eventsPath: filepath.Join(dir, "world_events.jsonl"),
}
}
func DefaultWorldState() WorldState {
now := time.Now().Unix()
return WorldState{
WorldID: "main-world",
Clock: Clock{
Tick: 0,
SimTimeUnix: now,
LastAdvance: now,
TickDuration: 30,
},
Player: PlayerState{
PlayerID: "player",
DisplayName: "Player",
CurrentLocation: "commons",
Status: "active",
},
Locations: map[string]Location{
"commons": {
ID: "commons",
Name: "Commons",
Neighbors: []string{"square"},
},
"square": {
ID: "square",
Name: "Square",
Neighbors: []string{"commons"},
},
},
GlobalFacts: map[string]interface{}{},
Entities: map[string]Entity{},
ActiveQuests: map[string]QuestState{},
RecentEvents: []WorldEvent{},
}
}
func (s *Store) LoadWorldState() (WorldState, error) {
if s == nil {
return DefaultWorldState(), nil
}
s.mu.RLock()
defer s.mu.RUnlock()
data, err := os.ReadFile(s.worldPath)
if err != nil {
if os.IsNotExist(err) {
return DefaultWorldState(), nil
}
return WorldState{}, err
}
var state WorldState
if err := json.Unmarshal(data, &state); err != nil {
return WorldState{}, err
}
if strings.TrimSpace(state.WorldID) == "" {
state.WorldID = "main-world"
}
if state.Clock.TickDuration <= 0 {
state.Clock.TickDuration = 30
}
if state.Locations == nil {
state.Locations = DefaultWorldState().Locations
}
if strings.TrimSpace(state.Player.PlayerID) == "" {
state.Player = DefaultWorldState().Player
}
if strings.TrimSpace(state.Player.CurrentLocation) == "" {
state.Player.CurrentLocation = "commons"
}
if state.GlobalFacts == nil {
state.GlobalFacts = map[string]interface{}{}
}
if state.Entities == nil {
state.Entities = map[string]Entity{}
}
if state.ActiveQuests == nil {
state.ActiveQuests = map[string]QuestState{}
}
return state, nil
}
func (s *Store) SaveWorldState(state WorldState) error {
if s == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if err := os.MkdirAll(s.dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.worldPath, data, 0644)
}
func (s *Store) LoadNPCStates() (map[string]NPCState, error) {
if s == nil {
return map[string]NPCState{}, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
data, err := os.ReadFile(s.npcPath)
if err != nil {
if os.IsNotExist(err) {
return map[string]NPCState{}, nil
}
return nil, err
}
items := map[string]NPCState{}
if err := json.Unmarshal(data, &items); err != nil {
return nil, err
}
return items, nil
}
func (s *Store) SaveNPCStates(items map[string]NPCState) error {
if s == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if items == nil {
items = map[string]NPCState{}
}
if err := os.MkdirAll(s.dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(items, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.npcPath, data, 0644)
}
func (s *Store) SaveNPCState(id string, state NPCState) error {
items, err := s.LoadNPCStates()
if err != nil {
return err
}
items[strings.TrimSpace(id)] = state
return s.SaveNPCStates(items)
}
func (s *Store) AppendWorldEvent(evt WorldEvent) error {
if s == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if err := os.MkdirAll(s.dir, 0755); err != nil {
return err
}
data, err := json.Marshal(evt)
if err != nil {
return err
}
f, err := os.OpenFile(s.eventsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(append(data, '\n'))
return err
}
func (s *Store) Events(limit int) ([]WorldEvent, error) {
if s == nil {
return nil, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
f, err := os.Open(s.eventsPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer f.Close()
var out []WorldEvent
scanner := bufio.NewScanner(f)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 2*1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
var evt WorldEvent
if err := json.Unmarshal([]byte(line), &evt); err != nil {
continue
}
out = append(out, evt)
}
if err := scanner.Err(); err != nil {
return nil, err
}
sort.Slice(out, func(i, j int) bool {
if out[i].Tick != out[j].Tick {
return out[i].Tick > out[j].Tick
}
return out[i].CreatedAt > out[j].CreatedAt
})
if limit > 0 && len(out) > limit {
out = out[:limit]
}
return out, nil
}