Browse Source

Reworked node to determin it's own depth

  This hiearchy based depth excludes a node only if that node is tagged
'root'.

  Tags have been changed to a more programatic array of strings.
Apollo 2 years ago
parent
commit
c19187e32a
14 changed files with 544 additions and 97 deletions
  1. 6 0
      .gitignore
  2. 38 0
      attributes.go
  3. 53 0
      engines.go
  4. 39 2
      main.go
  5. 48 7
      node.go
  6. 27 36
      node_test.go
  7. 61 0
      outfits.go
  8. 10 11
      parser.go
  9. 4 5
      parser_test.go
  10. 18 5
      profile.go
  11. 6 9
      profile_test.go
  12. 153 22
      ship.go
  13. 19 0
      sprite.go
  14. 62 0
      weapons.go

+ 6 - 0
.gitignore

@@ -9,3 +9,9 @@ old_*
 # Exclude fyne-cross (this is all generated)
 fyne-cross/
 
+# Exclude debug.json files
+_debug*
+
+# Exclude Test Output
++Test Dummy.txt
+

+ 38 - 0
attributes.go

@@ -0,0 +1,38 @@
+package main
+
+import "fmt"
+
+type Attributes struct {
+	node     *Node // Ref
+	Category string
+	Others   map[string]float64
+}
+
+func (a *Attributes) Parse(node *Node) error {
+	if node.Key() != "attributes" {
+		return fmt.Errorf("not attributes")
+	}
+	a.node = node
+	for _, kid := range node.Children {
+		if kid.Key() == "category" {
+			a.Category = kid.Value()
+		} else {
+			a.Others[kid.Key()] = kid.ValueFloat64()
+		}
+	}
+	return nil
+}
+
+func (a *Attributes) Update() error {
+	if a.node == nil {
+		return fmt.Errorf("no node, attributes")
+	}
+	a.node.RmAllChildren()
+	n := a.node.NewChild()
+	n.Set("category", a.Category)
+	for k, v := range a.Others {
+		n := a.node.NewChild()
+		n.Set(k, v)
+	}
+	return nil
+}

+ 53 - 0
engines.go

@@ -0,0 +1,53 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+)
+
+type Engine struct {
+	node  *Node  // Ref
+	Kind  string // engine, reverse engine, steering
+	Sides string // L = Left, R = Right
+	Pos   Vec2
+	Zoom  float64
+	Angle float64
+	Over  bool // false=under, true=over
+}
+
+func (e *Engine) Parse(node *Node) error {
+	if !strings.Contains(node.Key(), "engine") {
+		return fmt.Errorf("not a engine")
+	}
+	e.node = node
+	if strings.Contains(node.Key(), "reverse") {
+		e.Kind = "reverse engine"
+	} else if strings.Contains(node.Key(), "steering") {
+		e.Kind = "steering engine"
+	} else {
+		e.Kind = "engine"
+	}
+	e.Pos.Parse(node.Line)
+	if node.Len() == 0 {
+		return nil
+	}
+	for _, kid := range node.Children {
+		switch kid.Key() {
+		case "zoom":
+			e.Zoom = kid.ValueFloat64()
+		case "angle":
+			e.Angle = kid.ValueFloat64()
+		case "over":
+			e.Over = true
+		case "left":
+			if !strings.Contains(e.Sides, "L") {
+				e.Sides += "L"
+			}
+		case "right":
+			if !strings.Contains(e.Sides, "R") {
+				e.Sides += "R"
+			}
+		}
+	}
+	return nil
+}

+ 39 - 2
main.go

@@ -11,19 +11,29 @@ func main() {
 	SetupHome()
 	HOME.Show()
 	APP.Run()*/
-	p := Parser{}
+	p := Parser{Debug: false}
 	root, err := p.Load("Test Dummy.txt")
 	if err != nil {
 		fmt.Println("parser.load", err)
 		return
 	}
+	/*pay, err := json.MarshalIndent(root, "", "  ")
+	if err != nil {
+		fmt.Println("marshalindent", err)
+		return
+	}
+	err = os.WriteFile("_debug0.json", pay, 0660)
+	if err != nil {
+		fmt.Println("writefile", err)
+		return
+	}*/
 	me := Profile{}
 	err = me.Parse(root)
 	if err != nil {
 		fmt.Println("profile.parse", err)
 		return
 	}
-	fmt.Printf("%#v\n", me)
+	//fmt.Printf("%#v\n", me)
 	pay, err := json.MarshalIndent(me, "", "  ")
 	if err != nil {
 		fmt.Println("marshalindent", err)
@@ -34,4 +44,31 @@ func main() {
 		fmt.Println("writefile", err)
 		return
 	}
+	me.Ships[0].Attributes.Others["bunks"] = 100
+	me.Ships[0].Attributes.Others["cargo space"] = 100
+	me.Ships[0].Attributes.Others["required crew"] = 1
+	me.Ships[0].Name = "Test Me II"
+	me.Ships[0].Hull = int(me.Ships[0].Attributes.Others["hull"])
+	me.Ships[0].Shields = int(me.Ships[0].Attributes.Others["shields"])
+	me.Ships[0].Fuel = int(me.Ships[0].Attributes.Others["fuel capacity"])
+	err = me.Update()
+	if err != nil {
+		fmt.Println("profile.update", err)
+		return
+	}
+	err = p.Save("+Test Dummy.txt", root)
+	if err != nil {
+		fmt.Println("parser.save", err)
+		return
+	}
+	pay, err = json.MarshalIndent(me, "", "  ")
+	if err != nil {
+		fmt.Println("marshalindent", err)
+		return
+	}
+	err = os.WriteFile("_debug1.json", pay, 0660)
+	if err != nil {
+		fmt.Println("writefile", err)
+		return
+	}
 }

+ 48 - 7
node.go

@@ -5,24 +5,64 @@ import (
 	"log"
 	"strconv"
 	"strings"
+
+	"golang.org/x/exp/slices"
 )
 
 // A interconnected structure which connects to multiple entities
 //
 // Handles depth (in tabs) for the Endless sky save format (which is ugly)
 type Node struct {
-	Tag      string  // A tag, useful for identifying this node from other nodes in Children
-	Line     string  // The line of text for the node (the Parser removes the depth number of tabs)
-	Depth    int     // Number of tabs deep this node is (root and such are depth 0)
-	Parent   *Node   `json:"-"` // The parent of this node (used for traversing up the node tree), must be json ignored or recursive looping
-	Children []*Node // Any other nodes that can be tied to this node
+	Tags     []string // A tag, useful for identifying this node from other nodes in Children
+	Line     string   // The line of text for the node (the Parser removes the depth number of tabs)
+	Parent   *Node    `json:"-"` // The parent of this node (used for traversing up the node tree), must be json ignored or recursive looping
+	Children []*Node  // Any other nodes that can be tied to this node
+}
+
+func (n *Node) HasTag(tag string) bool {
+	return slices.Contains(n.Tags, tag)
+}
+
+func (n *Node) AddTag(tag string) {
+	if !n.HasTag(tag) {
+		n.Tags = append(n.Tags, strings.ToLower(tag))
+	}
+}
+
+func (n *Node) RmTag(tag string) {
+	if n.HasTag(tag) {
+		tags := []string{}
+		for _, t := range n.Tags {
+			if t != tag {
+				tags = append(tags, t)
+			}
+		}
+		n.Tags = tags
+	}
+}
+
+// How deep in tabs we are
+func (n *Node) Depth() int {
+	if n.Parent == nil {
+		return 0
+	}
+	depth := 0
+	at := n
+	for at.Parent != nil {
+		at = at.Parent
+		depth += 1
+	}
+	if at.HasTag("root") {
+		depth -= 1
+	}
+	return depth
 }
 
 // Returns the Endless sky representation (node depth is number of tabs)
 //
 // Useful for representing the node's line at the correct depth
 func (n *Node) String() string {
-	return fmt.Sprintf("%s%s", strings.Repeat("\t", n.Depth), n.Line)
+	return fmt.Sprintf("%s%s", strings.Repeat("\t", n.Depth()), n.Line)
 }
 
 // Strings everything (the current line plus any children)
@@ -92,8 +132,9 @@ func (n *Node) DetachFromParent() (*Node, error) {
 		p := n.Parent
 		index := -1
 		for idx, k := range p.Children {
-			if k.Line == n.Line && k.Depth == n.Depth && k.Tag == n.Tag && k.Len() == n.Len() {
+			if k.Line == n.Line && k.Depth() == n.Depth() && slices.Equal(k.Tags, n.Tags) && k.Len() == n.Len() {
 				index = idx
+				break
 			}
 		}
 		if index != -1 {

+ 27 - 36
node_test.go

@@ -1,65 +1,63 @@
 package main
 
-import "testing"
+import (
+	"testing"
+
+	"golang.org/x/exp/slices"
+)
 
 func TestNodeString(t *testing.T) {
 	n := Node{
-		Tag:   "1",
-		Line:  "Test",
-		Depth: 1,
+		Line: "Test",
 	}
-	if n.String() != "\tTest" {
+	n.AddTag("1")
+	if n.String() != "Test" {
 		t.Errorf("Expected 1 tab for depth=1, got '%s'", n.String())
 	}
 	n1 := n.NewChild()
 	n1.Line = "Hello World"
-	n1.Depth = 2
-	if n.StringAll() != "\tTest\n\t\tHello World\n" {
-		t.Errorf("Expected '\tTest\n\t\tHello World\n' got '%s'", n.StringAll())
+	if n.StringAll() != "Test\n\tHello World\n" {
+		t.Errorf("Expected 'Test\n\tHello World\n' got '%s'", n.StringAll())
 	}
 }
 
 func TestNodeAddChild(t *testing.T) {
 	n := Node{
-		Tag:   "1",
-		Line:  "Test",
-		Depth: 1,
+		Line: "Test",
 	}
+	n.AddTag("1")
 	n1 := Node{
-		Tag:   "2",
-		Line:  "Test 2",
-		Depth: 2,
+		Line: "Test 2",
 	}
+	n1.AddTag("2")
 	n.AddChild(&n1)
 	if len(n.Children) != 1 {
 		t.Errorf("Expected to add the child, but didn't (Children Count: %d)", len(n.Children))
 	}
-	if n1.Parent.Tag != n.Tag {
-		t.Errorf("Expected child's parent to have the same tag, expected '1', got '%s'", n1.Parent.Tag)
+	if !slices.Equal(n1.Parent.Tags, n.Tags) {
+		t.Errorf("Expected child's parent to have the same tag, expected '1', got '%s'", n1.Parent.Tags)
 	}
-	if n.Children[0].Tag != n1.Tag {
-		t.Errorf("Expected child's tag to be the same as other node's tag, expected '2', got '%s'", n.Children[0].Tag)
+	if !slices.Equal(n.Children[0].Tags, n1.Tags) {
+		t.Errorf("Expected child's tag to be the same as other node's tag, expected '2', got '%s'", n.Children[0].Tags)
 	}
 }
 
 func TestNodeNewChild(t *testing.T) {
 	n := Node{
-		Tag:   "1",
-		Line:  "Test",
-		Depth: 1,
+		Line: "Test",
 	}
+	n.AddTag("1")
 	n1 := n.NewChild()
-	n1.Tag = "2"
+	n1.AddTag("2")
 	n1.Line = "Test 2"
-	n1.Depth = 2
 	if len(n.Children) != 1 {
 		t.Errorf("Expected to add the child, but didn't (Children Count: %d)", len(n.Children))
 	}
 	if n.Children[0].Line != n1.Line {
 		t.Errorf("Expected 'Test 2', got '%s' ('%s')", n.Children[0].Line, n1.Line)
 	}
-	if n1.Depth != n.Children[0].Depth {
-		t.Errorf("Expected depth=2, got %d (%d)", n1.Depth, n.Children[0].Depth)
+	if n1.Depth() != 1 {
+		t.Errorf("Expected depth=1, got %d (%d)", n1.Depth(), n.Children[0].Depth())
 	}
 }
 
@@ -162,18 +160,14 @@ func TestNodeSetters(t *testing.T) {
 }
 
 func TestNodeChildActions(t *testing.T) {
-	n := Node{
-		Tag: "Root",
-	}
+	n := Node{}
+	n.AddTag("root")
 	n.Set("I am root", nil)
 	n1 := n.NewChild()
 	n1.Set("I am a child", 1)
-	n1.Depth = 1
 	n2 := n.NewChild()
 	n2.Set("I am a child", 2)
-	n2.Depth = 1
 	n2_1 := n2.NewChild()
-	n2_1.Depth = 2
 	n2_1.Set("I am a child", 3)
 	if n.Len() != 2 {
 		t.Errorf("Expected 2 children, got %d", len(n.Children))
@@ -213,17 +207,14 @@ func TestNodeChildActions(t *testing.T) {
 	// Add it back in
 	n.AddChild(n3)
 	// Now replace the first child with a new one
-	n3 = &Node{
-		Tag:   "Kid 1",
-		Depth: 1,
-	}
+	n3 = &Node{}
+	n3.AddTag("Kid 1")
 	n3.Set("I am a child", 1.1)
 	err = n.ReplaceChild(0, n3)
 	if err != nil {
 		t.Errorf("Unexpected error: %v", err)
 	}
 	n3_1 := n3.NewChild()
-	n3_1.Depth = 2
 	n3_1.Set("I am a child", 1.2)
 	if n.Len() != 2 {
 		t.Errorf("Expected 2 children, got %d", n.Len())

+ 61 - 0
outfits.go

@@ -0,0 +1,61 @@
+package main
+
+import "fmt"
+
+type OutfitList struct {
+	node *Node // Ref
+	List []OutfitListItem
+}
+
+func (ol *OutfitList) Parse(node *Node) error {
+	if node.Key() != "outfits" {
+		return fmt.Errorf("not a outfit list")
+	}
+	ol.node = node
+	ol.List = []OutfitListItem{}
+	for _, kid := range node.Children {
+		o := OutfitListItem{}
+		o.Parse(kid)
+		ol.AddOutfit(o)
+	}
+	return nil
+}
+
+func (ol *OutfitList) AddOutfit(outfit OutfitListItem) {
+	ol.List = append(ol.List, outfit)
+}
+
+func (ol *OutfitList) Update() error {
+	if ol.node == nil {
+		return fmt.Errorf("invalid node")
+	}
+	ol.node.RmAllChildren()
+	for _, outfit := range ol.List {
+		outfit.Generate(ol.node)
+	}
+	return nil
+}
+
+type OutfitListItem struct {
+	Name  string
+	Count int
+}
+
+func (o *OutfitListItem) Parse(node *Node) error {
+	o.Name = node.Key()
+	if CanInt(node.Value()) {
+		o.Count = node.ValueInt()
+	} else {
+		o.Count = 1
+	}
+	return nil
+}
+
+func (o *OutfitListItem) Generate(node *Node) {
+	n := node.NewChild()
+	if o.Count > 1 {
+		n.Set(o.Name, o.Count)
+	} else {
+		n.Line = n.Format(o.Name)
+	}
+}

+ 10 - 11
parser.go

@@ -15,10 +15,9 @@ type Parser struct {
 // Parses a given file returning a tree of Nodes
 func (p *Parser) Load(filename string) (Root *Node, err error) {
 	Root = &Node{
-		Tag:   "root",
-		Line:  filename,
-		Depth: -1,
+		Line: filename,
 	}
+	Root.AddTag("root")
 
 	file, err := os.Open(filename)
 	if err != nil {
@@ -58,23 +57,23 @@ func (p *Parser) parse(file *[]string, linenum int, node *Node) {
 		fmt.Printf("%4d | '%s'\nNXTL | '%s'\n", linenum, line, next_line)
 	}
 	kid := node.NewChild()
-	kid.Depth = FindDepth(line)
+	depth := FindDepth(line)
 	kid.Line = CleanLine(line)
-	if next_depth == kid.Depth {
+	if next_depth == depth {
 		if p.Debug {
-			fmt.Printf("Same Depth (%d)\n", node.Depth)
+			fmt.Printf("Same Depth (%d)\n", depth)
 		}
 		p.parse(file, linenum+1, node)
-	} else if next_depth > kid.Depth {
+	} else if next_depth > depth {
 		if p.Debug {
-			fmt.Printf("Deeper (%d->%d)\n", kid.Depth, next_depth)
+			fmt.Printf("Deeper (%d->%d)\n", depth, next_depth)
 		}
 		p.parse(file, linenum+1, kid)
-	} else if next_depth < kid.Depth {
-		diff := kid.Depth - next_depth
+	} else if next_depth < depth {
+		diff := depth - next_depth
 		at := node
 		if p.Debug {
-			fmt.Printf("Surface (%d<-%d) diff=%d\n", kid.Depth, next_depth, diff)
+			fmt.Printf("Surface (%d<-%d) diff=%d\n", depth, next_depth, diff)
 		}
 		for up := 0; up < diff; up++ {
 			if at.Parent != nil {

+ 4 - 5
parser_test.go

@@ -42,8 +42,8 @@ func TestParseLoad(t *testing.T) {
 	if err != nil {
 		t.Errorf("Unexpected error, %v", err)
 	}
-	if k.Line != "ship \"Little Fish\"" || k.Depth != 0 {
-		t.Errorf("Expected 'ship \"Little Fish\"' depth of 0, got '%s' (depth=%d)", k.Line, k.Depth)
+	if k.Line != "ship \"Little Fish\"" || k.Depth() != 0 {
+		t.Errorf("Expected 'ship \"Little Fish\"' depth of 0, got '%s' (depth=%d)", k.Line, k.Depth())
 	}
 	if k.Len() != 2 {
 		t.Fail()
@@ -57,8 +57,8 @@ func TestParseLoad(t *testing.T) {
 	if err != nil {
 		t.Errorf("Unexpected error, %v", err)
 	}
-	if k1.Line != "name \"Hello World\"" || k1.Depth != 1 {
-		t.Errorf("Expected 'name \"Hello World\"' depth 1, got '%s' (depth=%d)", k1.Line, k1.Depth)
+	if k1.Line != "name \"Hello World\"" || k1.Depth() != 1 {
+		t.Errorf("Expected 'name \"Hello World\"' depth 1, got '%s' (depth=%d)", k1.Line, k1.Depth())
 	}
 }
 
@@ -70,7 +70,6 @@ func TestParseSave(t *testing.T) {
 	n1.Line = "Tacocat"
 	n2 := n1.NewChild()
 	n2.Line = "Meow?"
-	n2.Depth = 1
 	p := Parser{}
 	err := p.Save("_tmp2.txt", &n)
 	if err != nil {

+ 18 - 5
profile.go

@@ -22,7 +22,7 @@ func (p *Profile) PlayTime() string {
 }
 
 func (p *Profile) Parse(node *Node) error {
-	if node.Tag != "root" {
+	if !node.HasTag("root") {
 		return fmt.Errorf("non-root node given, expected root node")
 	}
 	p.node = node
@@ -33,14 +33,21 @@ func (p *Profile) Parse(node *Node) error {
 		case "date":
 			p.Date = kid.Value()
 		case "system":
-			p.Location.Parse(kid)
+			if p.Location.System == "" {
+				p.Location.Parse(kid)
+			}
 		case "planet":
-			p.Location.Parse(kid)
+			if p.Location.Planet == "" {
+				p.Location.Parse(kid)
+			}
 		case "playtime":
 			p.Playtime = kid.ValueFloat64()
 		case "ship":
 			s := &Ship{}
-			s.Parse(kid)
+			err := s.Parse(kid)
+			if err != nil {
+				return err
+			}
 			p.Ships = append(p.Ships, s)
 		}
 	}
@@ -49,12 +56,18 @@ func (p *Profile) Parse(node *Node) error {
 
 func (p *Profile) Update() error {
 	if p.node == nil {
-		return fmt.Errorf("expected node, got none")
+		return fmt.Errorf("no node, profile")
 	}
 	err := p.Location.Update()
 	if err != nil {
 		return err
 	}
+	for _, s := range p.Ships {
+		err := s.Update()
+		if err != nil {
+			return err
+		}
+	}
 	for _, kid := range p.node.Children {
 		switch kid.Key() {
 		case "pilot":

+ 6 - 9
profile_test.go

@@ -6,10 +6,9 @@ func TestProfileParse(t *testing.T) {
 	// Setup the root node / test data
 	// Already have parser tests so manually make them instead
 	root := &Node{
-		Tag:   "root",
-		Line:  "testing",
-		Depth: -1,
+		Line: "testing",
 	} // This data is from Test Dummy.txt
+	root.AddTag("root")
 	n := root.NewChild()
 	n.Set("pilot", "Test Dummy")
 	n = root.NewChild()
@@ -68,10 +67,9 @@ func TestProfileUpdate(t *testing.T) {
 	// Setup the root node / test data
 	// Already have parser tests so manually make them instead
 	root := &Node{
-		Tag:   "root",
-		Line:  "testing",
-		Depth: -1,
+		Line: "testing",
 	} // This data is from Test Dummy.txt
+	root.AddTag("root")
 	n := root.NewChild()
 	n.Set("pilot", "Test Dummy")
 	n = root.NewChild()
@@ -109,10 +107,9 @@ func TestProfileUpdate(t *testing.T) {
 
 func TestProfileNonRoots(t *testing.T) {
 	n := &Node{
-		Tag:   "Test",
-		Line:  "Hello World",
-		Depth: 0,
+		Line: "Hello World",
 	}
+	n.AddTag("test")
 	p := Profile{}
 	err := p.Parse(n)
 	if err == nil {

+ 153 - 22
ship.go

@@ -8,16 +8,17 @@ type Ship struct {
 	Name       string
 	Sprite     Sprite // TODO: Make this a struct
 	Thumbnail  string
-	UUID       string                 // We should not allow this to be changed (it might be significant to Endless Sky)
-	Attributes map[string]interface{} // TODO: Make this a struct
-	Outfits    []string               // TODO: Make this a struct
+	UUID       string // We should not allow this to be changed (it might be significant to Endless Sky)
+	Attributes Attributes
+	Outfits    OutfitList
 	Crew       int
 	Fuel       int
 	Shields    int
 	Hull       int
 	Pos        Vec2
-	Engines    []string // TODO: Make this a struct
-	Gun        []string // TODO: Make this a struct
+	Engines    []Engine
+	Guns       []Gun
+	Turrets    []Turret
 	Leak       []string // TODO: Make this a struct
 	Explode    []string // TODO: Make this a struct
 	Location   Location
@@ -33,30 +34,35 @@ func (s *Ship) Parse(node *Node) error {
 	}
 	s.node = node
 	s.Model = node.Value()
-	s.Attributes = make(map[string]interface{})
+	s.Attributes = Attributes{
+		Others: make(map[string]float64),
+	}
+	s.Engines = []Engine{}
+	s.Guns = []Gun{}
+	s.Turrets = []Turret{}
+	s.Outfits = OutfitList{}
 	for _, kid := range node.Children {
 		switch kid.Key() {
 		case "name":
 			s.Name = kid.Value()
 		case "sprite":
-			s.Sprite.Parse(kid)
+			err := s.Sprite.Parse(kid)
+			if err != nil {
+				return err
+			}
 		case "thumbnail":
 			s.Thumbnail = kid.Value()
 		case "uuid":
 			s.UUID = kid.Value()
 		case "attributes":
-			for _, k := range kid.Children {
-				if CanFloat(k.Value()) {
-					s.Attributes[k.Key()] = k.ValueFloat64()
-				} else if CanInt(k.Value()) {
-					s.Attributes[k.Key()] = k.ValueInt()
-				} else {
-					s.Attributes[k.Key()] = k.Value()
-				}
+			err := s.Attributes.Parse(kid)
+			if err != nil {
+				return err
 			}
 		case "outfits":
-			for _, k := range kid.Children {
-				s.Outfits = append(s.Outfits, k.Line)
+			err := s.Outfits.Parse(kid)
+			if err != nil {
+				return err
 			}
 		case "crew":
 			s.Crew = kid.ValueInt()
@@ -67,24 +73,149 @@ func (s *Ship) Parse(node *Node) error {
 		case "hull":
 			s.Hull = kid.ValueInt()
 		case "position":
-			s.Pos.Parse(kid.Value())
+			err := s.Pos.Parse(kid.Value())
+			if err != nil {
+				return err
+			}
 		case "engine":
-			s.Engines = append(s.Engines, kid.Line)
+			e := Engine{}
+			err := e.Parse(kid)
+			if err != nil {
+				return err
+			}
+			s.Engines = append(s.Engines, e)
 		case "gun":
-			s.Gun = append(s.Gun, kid.Value())
+			g := Gun{}
+			err := g.Parse(kid)
+			if err != nil {
+				return err
+			}
+			s.Guns = append(s.Guns, g)
+		case "turret":
+			t := Turret{}
+			err := t.Parse(kid)
+			if err != nil {
+				return err
+			}
+			s.Turrets = append(s.Turrets, t)
 		case "leak":
 			s.Leak = append(s.Leak, kid.Value())
 		case "explode":
 			s.Explode = append(s.Explode, kid.Value())
 		case "system":
-			s.Location.Parse(kid)
+			if s.Location.System == "" {
+				s.Location.Parse(kid)
+			}
 		case "planet":
-			s.Location.Parse(kid)
+			if s.Location.Planet == "" {
+				s.Location.Parse(kid)
+			}
 		}
 	}
 	return nil
 }
 
 func (s *Ship) Update() error {
+	if s.node == nil {
+		return fmt.Errorf("no node, ship")
+	}
+	s.node.SetValue(s.Model)
+	err := s.Location.Update()
+	if err != nil {
+		return fmt.Errorf("ship, %v", err)
+	}
+	for _, kid := range s.node.Children {
+		switch kid.Key() {
+		case "name":
+			kid.SetValue(s.Name)
+		case "sprite":
+			err := s.Sprite.Update()
+			if err != nil {
+				return err
+			}
+		case "thumbnail":
+			kid.SetValue(s.Thumbnail)
+		case "attributes":
+			err := s.Attributes.Update()
+			if err != nil {
+				return err
+			}
+		case "outfits":
+			err := s.Outfits.Update()
+			if err != nil {
+				return err
+			}
+		case "crew":
+			kid.SetValue(s.Crew)
+		case "fuel":
+			kid.SetValue(s.Fuel)
+		case "shields":
+			kid.SetValue(s.Shields)
+		case "hull":
+			kid.SetValue(s.Hull)
+		case "position":
+			kid.SetValue(s.Pos.String())
+		case "engine":
+			_, err := kid.DetachFromParent()
+			if err != nil {
+				return err
+			}
+		case "gun":
+			_, err := kid.DetachFromParent()
+			if err != nil {
+				return err
+			}
+		case "turret":
+			_, err := kid.DetachFromParent()
+			if err != nil {
+				return err
+			}
+		}
+	}
+	for _, e := range s.Engines {
+		n := s.node.NewChild()
+		n.Set(e.Kind, e.Pos.String())
+		n1 := n.NewChild()
+		n1.Set("zoom", e.Zoom)
+		n1 = n.NewChild()
+		n1.Set("angle", e.Angle)
+		if e.Over {
+			n1 = n.NewChild()
+			n1.Line = "over"
+		} else {
+			n1 = n.NewChild()
+			n1.Line = "under"
+		}
+	}
+	for _, g := range s.Guns {
+		n := s.node.NewChild()
+		if g.Mount != "" {
+			n.Set("gun", fmt.Sprintf("%s %s", g.Pos.String(), g.Mount))
+		} else {
+			n.Set("gun", g.Pos.String())
+		}
+		if g.Over {
+			n1 := n.NewChild()
+			n1.Line = "over"
+		} else {
+			n1 := n.NewChild()
+			n1.Line = "under"
+		}
+	}
+	for _, t := range s.Turrets {
+		n := s.node.NewChild()
+		if t.Mount != "" {
+			n.Set("turret", fmt.Sprintf("%s %s", t.Pos.String(), t.Mount))
+		} else {
+			n.Set("turret", t.Pos.String())
+		}
+		if t.Over {
+			n1 := n.NewChild()
+			n1.Line = "over"
+		} else {
+			n1 := n.NewChild()
+			n1.Line = "under"
+		}
+	}
 	return nil
 }

+ 19 - 0
sprite.go

@@ -32,3 +32,22 @@ func (s *Sprite) Parse(node *Node) error {
 	}
 	return nil
 }
+
+func (s *Sprite) Update() error {
+	if s.node == nil {
+		return fmt.Errorf("no node, sprite")
+	}
+	s.node.RmAllChildren()
+	s.node.SetValue(s.Img)
+	if s.Animated {
+		n := s.node.NewChild()
+		n.Set("frame rate", s.FrameRate)
+		n = s.node.NewChild()
+		n.Set("delay", s.Delay)
+		if s.RandomStart {
+			n := s.node.NewChild()
+			n.Line = n.Format("random start frame")
+		}
+	}
+	return nil
+}

+ 62 - 0
weapons.go

@@ -0,0 +1,62 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+)
+
+type Gun struct {
+	node  *Node
+	Pos   Vec2
+	Mount string // might be empty for no mount
+	Over  bool   // false=under, true=over
+}
+
+func (g *Gun) Parse(node *Node) error {
+	if node.Key() != "gun" {
+		return fmt.Errorf("not a gun")
+	}
+	g.node = node
+	g.Pos.Parse(node.Line)
+	work := strings.Split(node.Value(), " ")
+	if len(work) >= 3 {
+		g.Mount = strings.Join(work[3:], " ")
+	}
+	if node.Len() != 0 {
+		for _, kid := range node.Children {
+			switch kid.Key() {
+			case "over":
+				g.Over = true
+			}
+		}
+	}
+	return nil
+}
+
+type Turret struct {
+	node  *Node
+	Pos   Vec2
+	Mount string // might be empty for no mount
+	Over  bool   // false=under, true=over
+}
+
+func (t *Turret) Parse(node *Node) error {
+	if node.Key() != "turret" {
+		return fmt.Errorf("not a turret")
+	}
+	t.node = node
+	t.Pos.Parse(node.Line)
+	work := strings.Split(node.Value(), " ")
+	if len(work) >= 3 {
+		t.Mount = strings.Join(work[3:], " ")
+	}
+	if node.Len() != 0 {
+		for _, kid := range node.Children {
+			switch kid.Key() {
+			case "over":
+				t.Over = true
+			}
+		}
+	}
+	return nil
+}