mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-19 10:57:28 +08:00
Refactor runtime around world core
This commit is contained in:
232
pkg/world/store.go
Normal file
232
pkg/world/store.go
Normal file
@@ -0,0 +1,232 @@
|
||||
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,
|
||||
},
|
||||
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 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
|
||||
}
|
||||
Reference in New Issue
Block a user