385 lines
8.5 KiB
Go
385 lines
8.5 KiB
Go
package settings
|
|
|
|
import (
|
|
"math"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/engine/module"
|
|
"aego/format/aetext"
|
|
"aego/modules/physics/world"
|
|
)
|
|
|
|
const (
|
|
ID = "physics"
|
|
Version = 1
|
|
MaxLayers = 32
|
|
DefaultLayer = "Default"
|
|
LayersList = "layers"
|
|
)
|
|
|
|
type Rule struct {
|
|
Layer string
|
|
With []string
|
|
}
|
|
|
|
type Project struct {
|
|
Gravity mathx.Vec2
|
|
HasGravity bool
|
|
Substeps int
|
|
VelocityIterations int
|
|
RelaxIterations int
|
|
ContactHertz float32
|
|
ContactDampingRatio float32
|
|
ContactPushVelocity float32
|
|
RestitutionThreshold float32
|
|
LinearSlop float32
|
|
SleepLinear float32
|
|
SleepAngular float32
|
|
SleepTime float32
|
|
LayerNames []string
|
|
Collide []Rule
|
|
}
|
|
|
|
func Default() Project {
|
|
return Project{}
|
|
}
|
|
|
|
func Section() module.SectionDesc[Project] {
|
|
return module.SectionDesc[Project]{
|
|
ID: ID,
|
|
Title: "Physics",
|
|
Order: 300,
|
|
Version: Version,
|
|
Default: Default,
|
|
Decode: Decode,
|
|
Encode: Encode,
|
|
Validate: (*Project).Validate,
|
|
Names: func(p *Project, list string) []string {
|
|
if list != LayersList {
|
|
return nil
|
|
}
|
|
l := p.Layers()
|
|
return l[:]
|
|
},
|
|
}
|
|
}
|
|
|
|
type namedFloat struct {
|
|
name string
|
|
v *float32
|
|
}
|
|
|
|
type namedInt struct {
|
|
name string
|
|
v *int
|
|
}
|
|
|
|
func (p *Project) floats() []namedFloat {
|
|
return []namedFloat{
|
|
{"contactHertz", &p.ContactHertz},
|
|
{"contactDampingRatio", &p.ContactDampingRatio},
|
|
{"contactPushVelocity", &p.ContactPushVelocity},
|
|
{"restitutionThreshold", &p.RestitutionThreshold},
|
|
{"linearSlop", &p.LinearSlop},
|
|
{"sleepLinear", &p.SleepLinear},
|
|
{"sleepAngular", &p.SleepAngular},
|
|
{"sleepTime", &p.SleepTime},
|
|
}
|
|
}
|
|
|
|
func (p *Project) ints() []namedInt {
|
|
return []namedInt{
|
|
{"substeps", &p.Substeps},
|
|
{"velocityIterations", &p.VelocityIterations},
|
|
{"relaxIterations", &p.RelaxIterations},
|
|
}
|
|
}
|
|
|
|
func (p *Project) Config() world.Config {
|
|
c := world.DefaultConfig()
|
|
if p.HasGravity {
|
|
c.Gravity = p.Gravity
|
|
}
|
|
for _, f := range []struct {
|
|
dst *int
|
|
v int
|
|
}{
|
|
{&c.Substeps, p.Substeps},
|
|
{&c.VelocityIterations, p.VelocityIterations},
|
|
{&c.RelaxIterations, p.RelaxIterations},
|
|
} {
|
|
if f.v > 0 {
|
|
*f.dst = f.v
|
|
}
|
|
}
|
|
for _, f := range []struct {
|
|
dst *float32
|
|
v float32
|
|
}{
|
|
{&c.ContactHertz, p.ContactHertz},
|
|
{&c.ContactDampingRatio, p.ContactDampingRatio},
|
|
{&c.ContactPushVelocity, p.ContactPushVelocity},
|
|
{&c.RestitutionThreshold, p.RestitutionThreshold},
|
|
{&c.LinearSlop, p.LinearSlop},
|
|
{&c.SleepLinear, p.SleepLinear},
|
|
{&c.SleepAngular, p.SleepAngular},
|
|
{&c.SleepTime, p.SleepTime},
|
|
} {
|
|
if f.v > 0 {
|
|
*f.dst = f.v
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
|
|
func ValidLayerName(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c == '_':
|
|
case c >= '0' && c <= '9' && i > 0:
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return s != "true" && s != "false"
|
|
}
|
|
|
|
func (p *Project) Layers() [MaxLayers]string {
|
|
var out [MaxLayers]string
|
|
if len(p.LayerNames) == 0 {
|
|
out[0] = DefaultLayer
|
|
return out
|
|
}
|
|
copy(out[:], p.LayerNames)
|
|
return out
|
|
}
|
|
|
|
func (p *Project) LayerIndex(name string) (int, bool) {
|
|
if name == "" {
|
|
return 0, false
|
|
}
|
|
for i, n := range p.Layers() {
|
|
if n == name {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (p *Project) named() uint32 {
|
|
var named uint32
|
|
for i, n := range p.Layers() {
|
|
if n != "" {
|
|
named |= 1 << i
|
|
}
|
|
}
|
|
return named
|
|
}
|
|
|
|
func (p *Project) Matrix() [MaxLayers]uint32 {
|
|
var m [MaxLayers]uint32
|
|
for i := range m {
|
|
m[i] = ^uint32(0)
|
|
}
|
|
unnamed := ^p.named()
|
|
for _, r := range p.Collide {
|
|
a, ok := p.LayerIndex(r.Layer)
|
|
if !ok {
|
|
continue
|
|
}
|
|
row := unnamed
|
|
for _, w := range r.With {
|
|
if b, ok := p.LayerIndex(w); ok {
|
|
row |= 1 << b
|
|
}
|
|
}
|
|
m[a] = row
|
|
}
|
|
for i := 0; i < MaxLayers; i++ {
|
|
for j := 0; j < MaxLayers; j++ {
|
|
if (m[i]>>j)&1 == 0 {
|
|
m[j] &^= 1 << i
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (p *Project) SetMatrix(m [MaxLayers]uint32) {
|
|
names := p.Layers()
|
|
named := p.named()
|
|
p.Collide = nil
|
|
for i, n := range names {
|
|
if n == "" || m[i]&named == named {
|
|
continue
|
|
}
|
|
r := Rule{Layer: n}
|
|
for j, w := range names {
|
|
if w != "" && (m[i]>>j)&1 == 1 {
|
|
r.With = append(r.With, w)
|
|
}
|
|
}
|
|
p.Collide = append(p.Collide, r)
|
|
}
|
|
}
|
|
|
|
func bad(msg string, fields ...log.Field) error {
|
|
return errs.New(errs.ProjectBadFile, "physics: "+msg, fields...)
|
|
}
|
|
|
|
func (p *Project) Validate() error {
|
|
if len(p.LayerNames) > MaxLayers {
|
|
return bad("more than 32 layers")
|
|
}
|
|
seen := map[string]bool{}
|
|
for i, n := range p.LayerNames {
|
|
if n == "" {
|
|
continue
|
|
}
|
|
if !ValidLayerName(n) {
|
|
return bad("layer "+n+" is not a valid name", log.F("index", i))
|
|
}
|
|
if seen[n] {
|
|
return bad("layer "+n+" is declared twice", log.F("layer", n))
|
|
}
|
|
seen[n] = true
|
|
}
|
|
keys := map[string]bool{}
|
|
for _, r := range p.Collide {
|
|
if _, ok := p.LayerIndex(r.Layer); !ok {
|
|
return bad("collide names an unknown layer "+r.Layer, log.F("layer", r.Layer))
|
|
}
|
|
if keys[r.Layer] {
|
|
return bad("collide lists layer "+r.Layer+" twice", log.F("layer", r.Layer))
|
|
}
|
|
keys[r.Layer] = true
|
|
for _, w := range r.With {
|
|
if _, ok := p.LayerIndex(w); !ok {
|
|
return bad("collide rule of "+r.Layer+" names an unknown layer "+w, log.F("layer", w))
|
|
}
|
|
}
|
|
}
|
|
for _, x := range p.ints() {
|
|
if *x.v < 0 {
|
|
return bad(x.name+" must not be negative", log.F("field", x.name))
|
|
}
|
|
}
|
|
for _, x := range p.floats() {
|
|
if f := float64(*x.v); math.IsNaN(f) || math.IsInf(f, 0) || f < 0 {
|
|
return bad(x.name+" must be a finite non-negative number", log.F("field", x.name))
|
|
}
|
|
}
|
|
if g := p.Gravity; math.IsNaN(float64(g.X)) || math.IsNaN(float64(g.Y)) || math.IsInf(float64(g.X), 0) || math.IsInf(float64(g.Y), 0) {
|
|
return bad("gravity is not finite")
|
|
}
|
|
return p.Config().Validate()
|
|
}
|
|
|
|
func nameOf(v aetext.Value) string {
|
|
if v.Kind == aetext.VString {
|
|
return v.Str
|
|
}
|
|
return v.Ident
|
|
}
|
|
|
|
func layerValue(name string) aetext.Value {
|
|
if ValidLayerName(name) {
|
|
return aetext.Ident(name)
|
|
}
|
|
return aetext.String(name)
|
|
}
|
|
|
|
func Decode(n *aetext.Node, p *Project) error {
|
|
fl, in := p.floats(), p.ints()
|
|
fields:
|
|
for _, f := range n.Fields {
|
|
switch f.Name {
|
|
case "gravity":
|
|
if f.Value.Kind != aetext.VList || len(f.Value.List) != 2 {
|
|
return bad("gravity must be [x, y]")
|
|
}
|
|
p.Gravity = mathx.Vec2{X: float32(f.Value.List[0].AsFloat()), Y: float32(f.Value.List[1].AsFloat())}
|
|
p.HasGravity = true
|
|
continue
|
|
case "layers":
|
|
if f.Value.Kind != aetext.VList {
|
|
return bad("layers must be a list of names")
|
|
}
|
|
p.LayerNames = p.LayerNames[:0]
|
|
for _, v := range f.Value.List {
|
|
p.LayerNames = append(p.LayerNames, nameOf(v))
|
|
}
|
|
continue
|
|
}
|
|
for _, x := range fl {
|
|
if x.name == f.Name {
|
|
*x.v = float32(f.Value.AsFloat())
|
|
continue fields
|
|
}
|
|
}
|
|
for _, x := range in {
|
|
if x.name == f.Name {
|
|
*x.v = int(f.Value.AsInt())
|
|
continue fields
|
|
}
|
|
}
|
|
return bad("unknown setting "+f.Name, log.F("field", f.Name))
|
|
}
|
|
for _, b := range n.Blocks {
|
|
if b.Head != "collide" {
|
|
return bad("unknown block "+b.Head, log.F("block", b.Head))
|
|
}
|
|
for _, f := range b.Fields {
|
|
if f.Value.Kind != aetext.VList {
|
|
return bad("collide rule of "+f.Name+" must be a list of layers", log.F("layer", f.Name))
|
|
}
|
|
r := Rule{Layer: f.Name}
|
|
for _, v := range f.Value.List {
|
|
r.With = append(r.With, nameOf(v))
|
|
}
|
|
p.Collide = append(p.Collide, r)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Encode(p *Project, n *aetext.Node) {
|
|
if p.HasGravity {
|
|
n.Fields = append(n.Fields, aetext.Field{Name: "gravity", Value: aetext.List(aetext.Float32(p.Gravity.X), aetext.Float32(p.Gravity.Y))})
|
|
}
|
|
for _, x := range p.ints() {
|
|
if *x.v != 0 {
|
|
n.Fields = append(n.Fields, aetext.Field{Name: x.name, Value: aetext.Int(int64(*x.v))})
|
|
}
|
|
}
|
|
for _, x := range p.floats() {
|
|
if *x.v != 0 {
|
|
n.Fields = append(n.Fields, aetext.Field{Name: x.name, Value: aetext.Float32(*x.v)})
|
|
}
|
|
}
|
|
if len(p.LayerNames) > 0 && !(len(p.LayerNames) == 1 && p.LayerNames[0] == DefaultLayer) {
|
|
list := aetext.Value{Kind: aetext.VList}
|
|
for _, name := range p.LayerNames {
|
|
list.List = append(list.List, layerValue(name))
|
|
}
|
|
n.Fields = append(n.Fields, aetext.Field{Name: "layers", Value: list})
|
|
}
|
|
if len(p.Collide) > 0 {
|
|
c := &aetext.Node{Head: "collide"}
|
|
for _, r := range p.Collide {
|
|
list := aetext.Value{Kind: aetext.VList}
|
|
for _, w := range r.With {
|
|
list.List = append(list.List, layerValue(w))
|
|
}
|
|
c.Fields = append(c.Fields, aetext.Field{Name: r.Layer, Value: list})
|
|
}
|
|
n.Blocks = append(n.Blocks, c)
|
|
}
|
|
}
|