305 lines
7.8 KiB
Go
305 lines
7.8 KiB
Go
package settings
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
|
|
"aego/core/mathx"
|
|
"aego/engine/problems"
|
|
"aego/format/aetext"
|
|
"aego/schema"
|
|
)
|
|
|
|
const (
|
|
ProfileHead = "profile"
|
|
ProfileVersion = 1
|
|
ProjectHead = "defaults"
|
|
versionAttr = "version"
|
|
)
|
|
|
|
type OwnerVersion struct {
|
|
Owner string
|
|
Version int
|
|
Migrate func(n *aetext.Node, from, to int) error
|
|
}
|
|
|
|
func (s *Store) encode(e *entry, v schema.Value) aetext.Value {
|
|
switch e.d.Kind {
|
|
case schema.KindBool:
|
|
return aetext.Bool(v.I != 0)
|
|
case schema.KindInt64:
|
|
return aetext.Int(v.I)
|
|
case schema.KindFloat64:
|
|
return aetext.Float64(v.F)
|
|
case schema.KindEnum:
|
|
if c, ok := s.choiceOf(e, v); ok && ValidPath(c.Name) {
|
|
return aetext.Ident(c.Name)
|
|
}
|
|
return aetext.Int(v.I)
|
|
case schema.KindVec2:
|
|
return aetext.List(aetext.Float32(v.V.X), aetext.Float32(v.V.Y))
|
|
case schema.KindColor:
|
|
return aetext.List(aetext.Float32(v.C.R), aetext.Float32(v.C.G), aetext.Float32(v.C.B), aetext.Float32(v.C.A))
|
|
case schema.KindList:
|
|
out := aetext.Value{Kind: aetext.VList, List: make([]aetext.Value, len(v.List))}
|
|
for i, x := range v.List {
|
|
out.List[i] = aetext.String(x.S)
|
|
}
|
|
return out
|
|
}
|
|
return aetext.String(v.S)
|
|
}
|
|
|
|
func floats(a aetext.Value, n int) ([]float32, error) {
|
|
if a.Kind != aetext.VList || len(a.List) != n {
|
|
return nil, errors.New("expected " + strconv.Itoa(n) + " numbers")
|
|
}
|
|
out := make([]float32, n)
|
|
for i, x := range a.List {
|
|
if x.Kind != aetext.VInt && x.Kind != aetext.VFloat {
|
|
return nil, errors.New("expected numbers")
|
|
}
|
|
out[i] = float32(x.AsFloat())
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Store) decode(e *entry, a aetext.Value) (schema.Value, error) {
|
|
num := a.Kind == aetext.VInt || a.Kind == aetext.VFloat
|
|
switch e.d.Kind {
|
|
case schema.KindBool:
|
|
if a.Kind == aetext.VBool {
|
|
return schema.Bool(a.Bool), nil
|
|
}
|
|
return schema.Value{}, errors.New("expected true or false")
|
|
case schema.KindInt64:
|
|
if num {
|
|
return schema.Int64(a.AsInt()), nil
|
|
}
|
|
return schema.Value{}, errors.New("expected a number")
|
|
case schema.KindFloat64:
|
|
if num {
|
|
return schema.Float64(a.AsFloat()), nil
|
|
}
|
|
return schema.Value{}, errors.New("expected a number")
|
|
case schema.KindEnum:
|
|
switch a.Kind {
|
|
case aetext.VIdent:
|
|
for _, c := range s.choices(e) {
|
|
if c.Name == a.Ident {
|
|
return c.Value, nil
|
|
}
|
|
}
|
|
return schema.Value{}, errors.New("unknown value " + a.Ident)
|
|
case aetext.VInt:
|
|
return schema.Enum(a.Int), nil
|
|
}
|
|
return schema.Value{}, errors.New("expected a name")
|
|
case schema.KindVec2:
|
|
f, err := floats(a, 2)
|
|
if err != nil {
|
|
return schema.Value{}, err
|
|
}
|
|
return schema.Vec2(mathx.Vec2{X: f[0], Y: f[1]}), nil
|
|
case schema.KindColor:
|
|
f, err := floats(a, 4)
|
|
if err != nil {
|
|
return schema.Value{}, err
|
|
}
|
|
return schema.Color(mathx.Color{R: f[0], G: f[1], B: f[2], A: f[3]}), nil
|
|
case schema.KindList:
|
|
if a.Kind != aetext.VList {
|
|
return schema.Value{}, errors.New("expected a list of strings")
|
|
}
|
|
out := schema.Value{Kind: schema.KindList, List: make([]schema.Value, len(a.List))}
|
|
for i, x := range a.List {
|
|
if x.Kind != aetext.VString {
|
|
return schema.Value{}, errors.New("expected a list of strings")
|
|
}
|
|
out.List[i] = schema.String(x.Str)
|
|
}
|
|
return out, nil
|
|
}
|
|
if a.Kind != aetext.VString {
|
|
return schema.Value{}, errors.New("expected a string")
|
|
}
|
|
return schema.String(a.Str), nil
|
|
}
|
|
|
|
func (s *Store) write(root *aetext.Node, user bool, pick func(Key, *entry) (schema.Value, bool)) {
|
|
blocks := map[string]*aetext.Node{}
|
|
get := func(name string) *aetext.Node {
|
|
if b := blocks[name]; b != nil {
|
|
return b
|
|
}
|
|
b := &aetext.Node{Head: name}
|
|
if o := s.owners[name]; user && o.version > 1 {
|
|
b.Attrs = []aetext.Field{{Name: versionAttr, Value: aetext.Int(int64(o.version))}}
|
|
}
|
|
blocks[name] = b
|
|
return b
|
|
}
|
|
for i := range s.list {
|
|
e := &s.list[i]
|
|
if v, ok := pick(Key(i+1), e); ok {
|
|
b := get(e.d.Owner)
|
|
b.Fields = append(b.Fields, aetext.Field{Name: e.d.Path(), Value: s.encode(e, v)})
|
|
}
|
|
}
|
|
for _, name := range s.order {
|
|
o := s.owners[name]
|
|
if user && o.newer != nil {
|
|
root.Blocks = append(root.Blocks, o.newer)
|
|
continue
|
|
}
|
|
extra := o.projExtra
|
|
if user {
|
|
extra = o.userExtra
|
|
}
|
|
if len(extra) > 0 {
|
|
b := get(name)
|
|
b.Fields = append(b.Fields, extra...)
|
|
}
|
|
if b := blocks[name]; b != nil {
|
|
root.Blocks = append(root.Blocks, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Store) EncodeProfile() []byte {
|
|
root := &aetext.Node{Head: ProfileHead, Attrs: []aetext.Field{{Name: versionAttr, Value: aetext.Int(ProfileVersion)}}}
|
|
s.write(root, true, func(_ Key, e *entry) (schema.Value, bool) {
|
|
return e.user, e.hasUser && !e.d.Flags.Has(Transient)
|
|
})
|
|
root.Blocks = append(root.Blocks, s.unknownUser...)
|
|
return aetext.Write(root)
|
|
}
|
|
|
|
func (s *Store) ProjectNode(vals map[Key]schema.Value) *aetext.Node {
|
|
root := &aetext.Node{Head: ProjectHead}
|
|
s.write(root, false, func(k Key, _ *entry) (schema.Value, bool) {
|
|
v, ok := vals[k]
|
|
return v, ok
|
|
})
|
|
root.Blocks = append(root.Blocks, s.unknownProject...)
|
|
if len(root.Blocks) == 0 {
|
|
return nil
|
|
}
|
|
return root
|
|
}
|
|
|
|
func (s *Store) ProjectValues() map[Key]schema.Value {
|
|
out := map[Key]schema.Value{}
|
|
for i := range s.list {
|
|
if e := &s.list[i]; e.hasProject {
|
|
out[Key(i+1)] = clone(e.project)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Store) read(root *aetext.Node, user bool, name string) (map[Key]schema.Value, []*aetext.Node) {
|
|
vals := map[Key]schema.Value{}
|
|
var unknown []*aetext.Node
|
|
need := Project
|
|
if user {
|
|
need = User
|
|
}
|
|
for _, b := range root.Blocks {
|
|
o, ok := s.owners[b.Head]
|
|
if !ok {
|
|
unknown = append(unknown, b.Clone())
|
|
continue
|
|
}
|
|
n := b
|
|
if user {
|
|
ver := 1
|
|
if a, ok := b.Attr(versionAttr); ok {
|
|
ver = int(a.AsInt())
|
|
}
|
|
switch {
|
|
case ver > o.version:
|
|
o.newer = b.Clone()
|
|
s.report("CFG006", problems.Warning, name+": "+b.Head+" settings come from a newer version and are kept as they are", "")
|
|
continue
|
|
case ver < o.version:
|
|
if o.migrate == nil {
|
|
s.report("CFG007", problems.Warning, name+": "+b.Head+" settings of version "+strconv.Itoa(ver)+" have no migration, defaults are used", "")
|
|
continue
|
|
}
|
|
n = b.Clone()
|
|
if err := o.migrate(n, ver, o.version); err != nil {
|
|
s.report("CFG007", problems.Warning, name+": "+b.Head+": "+err.Error(), "")
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
for _, f := range n.Fields {
|
|
k, ok := s.byID[b.Head+"."+f.Name]
|
|
if !ok {
|
|
if user {
|
|
o.userExtra = append(o.userExtra, f)
|
|
} else {
|
|
o.projExtra = append(o.projExtra, f)
|
|
}
|
|
continue
|
|
}
|
|
e := s.at(k)
|
|
if !e.d.Flags.Has(need) {
|
|
s.report("CFG002", problems.Warning, name+": "+e.d.ID+" cannot be set here", e.d.ID)
|
|
continue
|
|
}
|
|
v, err := s.decode(e, f.Value)
|
|
if err == nil {
|
|
v, err = s.check(e, v)
|
|
}
|
|
if err != nil {
|
|
s.report("CFG002", problems.Warning, name+": "+e.d.ID+": "+err.Error()+", the default is used", e.d.ID)
|
|
continue
|
|
}
|
|
vals[k] = v
|
|
}
|
|
}
|
|
return vals, unknown
|
|
}
|
|
|
|
func (s *Store) LoadProject(n *aetext.Node, name string) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
vals, unknown := s.read(n, false, name)
|
|
s.unknownProject = unknown
|
|
for k, v := range vals {
|
|
e := s.at(k)
|
|
e.project, e.hasProject = v, true
|
|
e.recompute()
|
|
}
|
|
}
|
|
|
|
func (s *Store) LoadProfile(data []byte, name string) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
doc, err := aetext.Parse(data, name)
|
|
if err == nil && doc.Head != ProfileHead {
|
|
err = bad(name, "is not a settings profile")
|
|
}
|
|
if err != nil {
|
|
s.report("CFG001", problems.Warning, name+": the settings profile is damaged, defaults are used", "")
|
|
return err
|
|
}
|
|
if a, ok := doc.Attr(versionAttr); ok && int(a.AsInt()) > ProfileVersion {
|
|
s.readOnly = true
|
|
s.report("CFG006", problems.Warning, name+": the profile comes from a newer version, it is read-only", "")
|
|
return nil
|
|
}
|
|
vals, unknown := s.read(doc, true, name)
|
|
s.unknownUser = unknown
|
|
for k, v := range vals {
|
|
e := s.at(k)
|
|
e.user, e.hasUser = v, true
|
|
e.recompute()
|
|
}
|
|
return nil
|
|
}
|