diff --git a/Makefile b/Makefile
index b73faf0..3aedd83 100644
--- a/Makefile
+++ b/Makefile
@@ -18,12 +18,9 @@ gen:
 	protoc --proto_path proto --go_out=plugins=grpc,paths=source_relative:proto/ proto/roveapi/roveapi.proto
 
 test:
-	@echo Unit tests
-	go test -v ./...
-
-	@echo Integration tests
-	docker-compose up --build --exit-code-from=rove-tests --abort-on-container-exit rove-tests
-	docker-compose down
+	@echo Run unit and integration tests
+	docker-compose -f docker-compose-test.yml up --build --exit-code-from=rove-tests --abort-on-container-exit rove-tests
+	docker-compose -f docker-compose-test.yml down
 	go tool cover -html=/tmp/coverage-data/c.out -o /tmp/coverage.html
 	
 	@echo Done, coverage data can be found in /tmp/coverage.html
diff --git a/README.md b/README.md
index 2aff518..aad1a6f 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,8 @@
 Rove
 ====
-![Tests](https://github.com/mdiluz/rove/workflows/Tests/badge.svg) ![Docker](https://github.com/mdiluz/rove/workflows/Docker/badge.svg) [![rove](https://snapcraft.io//rove/badge.svg)](https://snapcraft.io/rove)
 
-![Rove](https://github.com/mdiluz/rove/blob/master/data/icon.svg)
+![Rove](data/icon.svg)
 
 Rove is an asynchronous nomadic game about exploring as part of a loose community.
 
-This repository contains the source code for the `rove-server` deployment and the `rove` command line client. See [mdiluz.github.io/rove](https://mdiluz.github.io/rove/) for game details, and [roveapi.proto](https://github.com/mdiluz/rove/blob/master/proto/roveapi/roveapi.proto) for the current server-client API.
+This repository contains the source code for the `rove-server` deployment and the `rove` command line client. See [mdiluz.github.io/rove](https://mdiluz.github.io/rove/) for game details, and [roveapi.proto](proto/roveapi/roveapi.proto) for the current server-client API.
diff --git a/cmd/rove-server/internal/routes.go b/cmd/rove-server/internal/routes.go
index 0fb41d8..69dd820 100644
--- a/cmd/rove-server/internal/routes.go
+++ b/cmd/rove-server/internal/routes.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"log"
 
-	"github.com/mdiluz/rove/pkg/rove"
 	"github.com/mdiluz/rove/pkg/version"
 	"github.com/mdiluz/rove/proto/roveapi"
 )
@@ -35,7 +34,7 @@ func (s *Server) Register(ctx context.Context, req *roveapi.RegisterRequest) (*r
 		return nil, fmt.Errorf("empty account name")
 	}
 
-	if acc, err := s.accountant.RegisterAccount(req.Name); err != nil {
+	if acc, err := s.world.Accountant.RegisterAccount(req.Name); err != nil {
 		return nil, err
 
 	} else if _, err := s.SpawnRoverForAccount(req.Name); err != nil {
@@ -58,13 +57,13 @@ func (s *Server) Register(ctx context.Context, req *roveapi.RegisterRequest) (*r
 func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (response *roveapi.StatusResponse, err error) {
 	log.Printf("Handling status request: %s\n", req.Account.Name)
 
-	if valid, err := s.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
+	if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
 		return nil, err
 
 	} else if !valid {
 		return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
 
-	} else if resp, err := s.accountant.GetValue(req.Account.Name, "rover"); err != nil {
+	} else if resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover"); err != nil {
 		return nil, err
 
 	} else if rover, err := s.world.GetRover(resp); err != nil {
@@ -76,40 +75,7 @@ func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (respon
 			inv = append(inv, byte(i.Type))
 		}
 
-		i, q := s.world.RoverCommands(resp)
-		var incoming, queued []*roveapi.Command
-		for _, i := range i {
-			c := &roveapi.Command{
-				Command: i.Command,
-			}
-			switch i.Command {
-			case roveapi.CommandType_move:
-				c.Data = &roveapi.Command_Bearing{
-					Bearing: i.Bearing,
-				}
-			case roveapi.CommandType_broadcast:
-				c.Data = &roveapi.Command_Message{
-					Message: i.Message,
-				}
-			}
-			incoming = append(incoming, c)
-		}
-		for _, q := range q {
-			c := &roveapi.Command{
-				Command: q.Command,
-			}
-			switch q.Command {
-			case roveapi.CommandType_move:
-				c.Data = &roveapi.Command_Bearing{
-					Bearing: q.Bearing,
-				}
-			case roveapi.CommandType_broadcast:
-				c.Data = &roveapi.Command_Message{
-					Message: q.Message,
-				}
-			}
-			queued = append(queued, c)
-		}
+		queued := s.world.RoverCommands(resp)
 		var logs []*roveapi.Log
 		for _, log := range rover.Logs {
 			logs = append(logs, &roveapi.Log{
@@ -119,21 +85,29 @@ func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (respon
 		}
 
 		response = &roveapi.StatusResponse{
-			Name: rover.Name,
-			Position: &roveapi.Vector{
-				X: int32(rover.Pos.X),
-				Y: int32(rover.Pos.Y),
+			Readings: &roveapi.RoverReadings{
+				Position: &roveapi.Vector{
+					X: int32(rover.Pos.X),
+					Y: int32(rover.Pos.Y),
+				},
+				Logs: logs,
+				Wind: s.world.Wind,
+			},
+			Spec: &roveapi.RoverSpecifications{
+				Name:             rover.Name,
+				Range:            int32(rover.Range),
+				Capacity:         int32(rover.Capacity),
+				MaximumIntegrity: int32(rover.MaximumIntegrity),
+				MaximumCharge:    int32(rover.MaximumCharge),
+			},
+			Status: &roveapi.RoverStatus{
+				Bearing:        rover.Bearing,
+				Inventory:      inv,
+				Integrity:      int32(rover.Integrity),
+				Charge:         int32(rover.Charge),
+				QueuedCommands: queued,
+				SailPosition:   rover.SailPosition,
 			},
-			Range:            int32(rover.Range),
-			Inventory:        inv,
-			Capacity:         int32(rover.Capacity),
-			Integrity:        int32(rover.Integrity),
-			MaximumIntegrity: int32(rover.MaximumIntegrity),
-			Charge:           int32(rover.Charge),
-			MaximumCharge:    int32(rover.MaximumCharge),
-			IncomingCommands: incoming,
-			QueuedCommands:   queued,
-			Logs:             logs,
 		}
 	}
 	return response, nil
@@ -143,7 +117,7 @@ func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (respon
 func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi.RadarResponse, error) {
 	log.Printf("Handling radar request: %s\n", req.Account.Name)
 
-	if valid, err := s.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
+	if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
 		return nil, err
 
 	} else if !valid {
@@ -152,7 +126,7 @@ func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi
 
 	response := &roveapi.RadarResponse{}
 
-	resp, err := s.accountant.GetValue(req.Account.Name, "rover")
+	resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
 	if err != nil {
 		return nil, err
 
@@ -175,33 +149,19 @@ func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi
 func (s *Server) Command(ctx context.Context, req *roveapi.CommandRequest) (*roveapi.CommandResponse, error) {
 	log.Printf("Handling command request: %s and %+v\n", req.Account.Name, req.Commands)
 
-	if valid, err := s.accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
+	if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
 		return nil, err
 
 	} else if !valid {
 		return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
 	}
 
-	resp, err := s.accountant.GetValue(req.Account.Name, "rover")
+	resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
 	if err != nil {
 		return nil, err
 	}
 
-	var cmds []rove.Command
-	for _, c := range req.Commands {
-		n := rove.Command{
-			Command: c.Command,
-		}
-		switch c.Command {
-		case roveapi.CommandType_move:
-			n.Bearing = c.GetBearing()
-		case roveapi.CommandType_broadcast:
-			n.Message = c.GetMessage()
-		}
-		cmds = append(cmds, n)
-	}
-
-	if err := s.world.Enqueue(resp, cmds...); err != nil {
+	if err := s.world.Enqueue(resp, req.Commands...); err != nil {
 		return nil, err
 	}
 
diff --git a/cmd/rove-server/internal/server.go b/cmd/rove-server/internal/server.go
index 102c30d..e2b7e16 100644
--- a/cmd/rove-server/internal/server.go
+++ b/cmd/rove-server/internal/server.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"log"
 	"net"
+	"os"
+	"path"
 	"sync"
 
 	"github.com/mdiluz/rove/pkg/persistence"
@@ -11,8 +13,12 @@ import (
 	"github.com/mdiluz/rove/proto/roveapi"
 	"github.com/robfig/cron"
 	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/reflection"
 )
 
+var cert = os.Getenv("CERT_NAME")
+
 const (
 	// PersistentData will allow the server to load and save it's state
 	PersistentData = iota
@@ -27,9 +33,6 @@ type Server struct {
 	// Internal state
 	world *rove.World
 
-	// Accountant
-	accountant Accountant
-
 	// gRPC server
 	netListener net.Listener
 	grpcServ    *grpc.Server
@@ -80,7 +83,6 @@ func NewServer(opts ...ServerOption) *Server {
 		persistence: EphemeralData,
 		schedule:    cron.New(),
 		world:       rove.NewWorld(32),
-		accountant:  NewSimpleAccountant(),
 	}
 
 	// Apply all options
@@ -107,8 +109,22 @@ func (s *Server) Initialise(fillWorld bool) (err error) {
 	if err != nil {
 		log.Fatalf("failed to listen: %v", err)
 	}
-	s.grpcServ = grpc.NewServer()
+
+	// Load TLS
+	var opts []grpc.ServerOption
+	if len(os.Getenv("NO_TLS")) == 0 {
+		pem := path.Join("/etc/letsencrypt/live/", cert, "fullchain.pem")
+		key := path.Join("/etc/letsencrypt/live/", cert, "privkey.pem")
+		creds, err := credentials.NewServerTLSFromFile(pem, key)
+		if err != nil {
+			log.Fatalf("failed to setup TLS: %v", err)
+		}
+		opts = append(opts, grpc.Creds(creds))
+	}
+
+	s.grpcServ = grpc.NewServer(opts...)
 	roveapi.RegisterRoveServer(s.grpcServ, s)
+	reflection.Register(s.grpcServ)
 
 	return nil
 }
@@ -131,8 +147,8 @@ func (s *Server) Run() {
 
 			log.Println("Executing server tick")
 
-			// Run the command queues
-			s.world.ExecuteCommandQueues()
+			// Tick the world
+			s.world.Tick()
 
 			// Save out the new world state
 			if err := s.SaveWorld(); err != nil {
@@ -188,7 +204,7 @@ func (s *Server) SaveWorld() error {
 	if s.persistence == PersistentData {
 		s.world.RLock()
 		defer s.world.RUnlock()
-		if err := persistence.SaveAll("world", s.world, "accounts", s.accountant); err != nil {
+		if err := persistence.SaveAll("world", s.world); err != nil {
 			return fmt.Errorf("failed to save out persistent data: %s", err)
 		}
 	}
@@ -200,7 +216,7 @@ func (s *Server) LoadWorld() error {
 	if s.persistence == PersistentData {
 		s.world.Lock()
 		defer s.world.Unlock()
-		if err := persistence.LoadAll("world", &s.world, "accounts", &s.accountant); err != nil {
+		if err := persistence.LoadAll("world", &s.world); err != nil {
 			return err
 		}
 	}
@@ -209,22 +225,10 @@ func (s *Server) LoadWorld() error {
 
 // SpawnRoverForAccount spawns the rover rover for an account
 func (s *Server) SpawnRoverForAccount(account string) (string, error) {
-	inst, err := s.world.SpawnRover()
+	inst, err := s.world.SpawnRover(account)
 	if err != nil {
 		return "", err
 	}
 
-	err = s.accountant.AssignData(account, "rover", inst)
-	if err != nil {
-		log.Printf("Failed to assign rover to account, %s", err)
-
-		// Try and clear up the rover
-		if err := s.world.DestroyRover(inst); err != nil {
-			log.Printf("Failed to destroy rover after failed rover assign: %s", err)
-		}
-
-		return "", err
-	}
-
 	return inst, nil
 }
diff --git a/cmd/rove-server/internal/server_test.go b/cmd/rove-server/internal/server_test.go
index 36db679..40d4b80 100644
--- a/cmd/rove-server/internal/server_test.go
+++ b/cmd/rove-server/internal/server_test.go
@@ -1,6 +1,7 @@
 package internal
 
 import (
+	"os"
 	"testing"
 )
 
@@ -30,6 +31,7 @@ func TestNewServer_OptionPersistentData(t *testing.T) {
 }
 
 func TestServer_Run(t *testing.T) {
+	os.Setenv("NO_TLS", "1")
 	server := NewServer()
 	if server == nil {
 		t.Error("Failed to create server")
@@ -45,6 +47,7 @@ func TestServer_Run(t *testing.T) {
 }
 
 func TestServer_RunPersistentData(t *testing.T) {
+	os.Setenv("NO_TLS", "1")
 	server := NewServer(OptionPersistentData())
 	if server == nil {
 		t.Error("Failed to create server")
diff --git a/cmd/rove/internal/glyph.go b/cmd/rove/internal/glyph.go
index 1a26f5d..4a774e7 100644
--- a/cmd/rove/internal/glyph.go
+++ b/cmd/rove/internal/glyph.go
@@ -22,6 +22,12 @@ const (
 	// GlyphRoverLive represents a live rover
 	GlyphRoverLive = Glyph('R')
 
+	// GlyphRoverDormant represents a dormant rover
+	GlyphRoverDormant = Glyph('r')
+
+	// GlyphRoverParts represents spare rover parts
+	GlyphRoverParts = Glyph('*')
+
 	// GlyphRockSmall is a small stashable rock
 	GlyphRockSmall = Glyph('o')
 
@@ -51,8 +57,12 @@ func ObjectGlyph(o roveapi.Object) Glyph {
 		return GlyphRoverLive
 	case roveapi.Object_RockSmall:
 		return GlyphRockSmall
+	case roveapi.Object_RoverDormant:
+		return GlyphRoverDormant
 	case roveapi.Object_RockLarge:
 		return GlyphRockLarge
+	case roveapi.Object_RoverParts:
+		return GlyphRoverParts
 	}
 
 	log.Fatalf("Unknown object type: %c", o)
diff --git a/cmd/rove/main.go b/cmd/rove/main.go
index bd98d52..f1e993d 100644
--- a/cmd/rove/main.go
+++ b/cmd/rove/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
@@ -8,6 +9,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"strconv"
 	"time"
 
 	"github.com/mdiluz/rove/cmd/rove/internal"
@@ -15,6 +17,7 @@ import (
 	"github.com/mdiluz/rove/proto/roveapi"
 	"golang.org/x/net/context"
 	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
 )
 
 var home = os.Getenv("HOME")
@@ -22,38 +25,44 @@ var defaultDataPath = path.Join(home, ".local/share/")
 
 // Command usage
 func printUsage() {
-	fmt.Fprintf(os.Stderr, "Usage: rove COMMAND [ARGS...]\n")
-	fmt.Fprintln(os.Stderr, "\nCommands")
-	fmt.Fprintln(os.Stderr, "\tserver-status              prints the server status")
-	fmt.Fprintln(os.Stderr, "\tregister NAME              registers an account and stores it (use with -name)")
-	fmt.Fprintln(os.Stderr, "\tcommand COMMAND [VAL...]   issue commands to rover, accepts multiple, see below")
-	fmt.Fprintln(os.Stderr, "\tradar                      gathers radar data for the current rover")
-	fmt.Fprintln(os.Stderr, "\tstatus                     gets status info for current rover")
-	fmt.Fprintln(os.Stderr, "\tconfig [HOST]              outputs the local config info, optionally sets host")
-	fmt.Fprintln(os.Stderr, "\thelp                       outputs this usage information")
-	fmt.Fprintln(os.Stderr, "\tversion                    outputs version info")
-	fmt.Fprintln(os.Stderr, "\nRover commands:")
-	fmt.Fprintln(os.Stderr, "\tmove BEARING               moves the rover in the chosen direction")
-	fmt.Fprintln(os.Stderr, "\tstash                      stores the object at the rover location in the inventory")
-	fmt.Fprintln(os.Stderr, "\trepair                     uses an inventory object to repair the rover")
-	fmt.Fprintln(os.Stderr, "\trecharge                   wait a tick to recharge the rover")
-	fmt.Fprintln(os.Stderr, "\tbroadcast MSG              broadcast a simple ASCII triplet to nearby rovers")
-	fmt.Fprintln(os.Stderr, "\nEnvironment")
-	fmt.Fprintln(os.Stderr, "\tROVE_USER_DATA             path to user data, defaults to "+defaultDataPath)
+	fmt.Fprintln(os.Stderr, "Usage: rove ARG [OPT...]")
+	fmt.Fprintf(os.Stderr, "\n")
+	fmt.Fprintln(os.Stderr, "Arguments:")
+	fmt.Fprintln(os.Stderr, "\tversion                       outputs version")
+	fmt.Fprintln(os.Stderr, "\thelp                          outputs this usage text")
+	fmt.Fprintln(os.Stderr, "\tconfig [HOST]                 outputs the local config, optionally sets host")
+	fmt.Fprintln(os.Stderr, "\tserver-status                 prints the server status")
+	fmt.Fprintln(os.Stderr, "\tregister NAME                 registers an account and spawns a rover")
+	fmt.Fprintln(os.Stderr, "\tradar                         prints radar data in ASCII form")
+	fmt.Fprintln(os.Stderr, "\tstatus                        gets rover status")
+	fmt.Fprintln(os.Stderr, "\tcommand CMD [VAL...] [REPEAT] sets the command queue, accepts multiple in sequence")
+	fmt.Fprintf(os.Stderr, "\n")
+	fmt.Fprintln(os.Stderr, "Rover commands:")
+	fmt.Fprintln(os.Stderr, "\ttoggle         toggles the current sail mode")
+	fmt.Fprintln(os.Stderr, "\tstash          stores the object at the rover location in the inventory")
+	fmt.Fprintln(os.Stderr, "\trepair         repairs the rover using inventory item")
+	fmt.Fprintln(os.Stderr, "\tbroadcast MSG  broadcast a simple ASCII triplet to nearby rovers")
+	fmt.Fprintln(os.Stderr, "\tsalvage        salvages a dormant rover for parts")
+	fmt.Fprintln(os.Stderr, "\ttransfer       transfer's control into a dormant rover")
+	fmt.Fprintln(os.Stderr, "\tupgrade SPEC   spends rover parts to upgrade one rover spec (capacity, range, integrity, charge")
+	fmt.Fprintln(os.Stderr, "\twait           waits before performing the next command")
+	fmt.Fprintf(os.Stderr, "\n")
+	fmt.Fprintln(os.Stderr, "Environment")
+	fmt.Fprintln(os.Stderr, "\tROVE_USER_DATA        path to user data, defaults to "+defaultDataPath)
 }
 
 const gRPCport = 9090
 
 // Account stores data for an account
 type Account struct {
-	Name   string `json:"name"`
-	Secret string `json:"secret"`
+	Name   string
+	Secret string
 }
 
 // Config is used to store internal data
 type Config struct {
-	Host    string  `json:"host,omitempty"`
-	Account Account `json:"account,omitempty"`
+	Host    string
+	Account Account
 }
 
 // ConfigPath returns the configuration path
@@ -128,12 +137,20 @@ func BearingFromString(s string) roveapi.Bearing {
 	switch s {
 	case "N":
 		return roveapi.Bearing_North
+	case "NE":
+		return roveapi.Bearing_NorthEast
 	case "E":
 		return roveapi.Bearing_East
+	case "SE":
+		return roveapi.Bearing_SouthEast
 	case "S":
 		return roveapi.Bearing_South
+	case "SW":
+		return roveapi.Bearing_SouthWest
 	case "W":
 		return roveapi.Bearing_West
+	case "NW":
+		return roveapi.Bearing_NorthWest
 	}
 	return roveapi.Bearing_BearingUnknown
 }
@@ -171,8 +188,15 @@ func InnerMain(command string, args ...string) error {
 		return fmt.Errorf("no host set in %s, set one with '%s config {HOST}'", ConfigPath(), os.Args[0])
 	}
 
+	var opts []grpc.DialOption
+	if len(os.Getenv("NO_TLS")) == 0 {
+		opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
+	} else {
+		opts = append(opts, grpc.WithInsecure())
+	}
+
 	// Set up the server
-	clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), grpc.WithInsecure())
+	clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), opts...)
 	if err != nil {
 		return err
 	}
@@ -224,22 +248,22 @@ func InnerMain(command string, args ...string) error {
 		// Iterate through each command
 		var commands []*roveapi.Command
 		for i := 0; i < len(args); i++ {
+
+			var cmd *roveapi.Command
 			switch args[i] {
-			case "move":
+			case "turn":
 				i++
 				if len(args) == i {
-					return fmt.Errorf("move command must be passed bearing")
+					return fmt.Errorf("turn command must be passed a compass bearing")
 				}
-				var b roveapi.Bearing
-				if b = BearingFromString(args[i]); b == roveapi.Bearing_BearingUnknown {
-					return fmt.Errorf("unrecognised bearing: %s", args[i])
+				b := BearingFromString(args[i])
+				if b == roveapi.Bearing_BearingUnknown {
+					return fmt.Errorf("turn command must be given a valid bearing %s", args[i])
+				}
+				cmd = &roveapi.Command{
+					Command: roveapi.CommandType_turn,
+					Bearing: b,
 				}
-				commands = append(commands,
-					&roveapi.Command{
-						Command: roveapi.CommandType_move,
-						Data:    &roveapi.Command_Bearing{Bearing: b},
-					},
-				)
 			case "broadcast":
 				i++
 				if len(args) == i {
@@ -247,20 +271,51 @@ func InnerMain(command string, args ...string) error {
 				} else if len(args[i]) > 3 {
 					return fmt.Errorf("broadcast command must be given ASCII triplet of 3 or less: %s", args[i])
 				}
-				commands = append(commands,
-					&roveapi.Command{
-						Command: roveapi.CommandType_broadcast,
-						Data:    &roveapi.Command_Message{Message: []byte(args[i])},
-					},
-				)
+				cmd = &roveapi.Command{
+					Command: roveapi.CommandType_broadcast,
+					Data:    []byte(args[i]),
+				}
+			case "upgrade":
+				i++
+				if len(args) == i {
+					return fmt.Errorf("upgrade command must be passed a spec to upgrade")
+				}
+				var u roveapi.RoverUpgrade
+				switch args[i] {
+				case "capacity":
+					u = roveapi.RoverUpgrade_Capacity
+				case "range":
+					u = roveapi.RoverUpgrade_Range
+				case "integrity":
+					u = roveapi.RoverUpgrade_MaximumIntegrity
+				case "charge":
+					u = roveapi.RoverUpgrade_MaximumCharge
+				default:
+					return fmt.Errorf("upgrade command must be passed a known upgrade spec")
+				}
+				cmd = &roveapi.Command{
+					Command: roveapi.CommandType_upgrade,
+					Upgrade: u,
+				}
 			default:
 				// By default just use the command literally
-				commands = append(commands,
-					&roveapi.Command{
-						Command: roveapi.CommandType(roveapi.CommandType_value[args[i]]),
-					},
-				)
+				cmd = &roveapi.Command{
+					Command: roveapi.CommandType(roveapi.CommandType_value[args[i]]),
+				}
 			}
+
+			// Try and convert the next command to a number
+			number := 0
+			if len(args) > i+1 {
+				num, err := strconv.Atoi(args[i+1])
+				if err == nil {
+					number = num
+					i++
+				}
+			}
+			cmd.Repeat = int32(number)
+
+			commands = append(commands, cmd)
 		}
 
 		_, err := client.Command(ctx, &roveapi.CommandRequest{
@@ -348,6 +403,15 @@ func InnerMain(command string, args ...string) error {
 func main() {
 	// Bail without any args
 	if len(os.Args) == 1 {
+		fmt.Fprintf(os.Stderr, "\n")
+		fmt.Fprintln(os.Stderr, "m mm   mmm   m   m   mmm")
+		fmt.Fprintln(os.Stderr, "#\"  \" #\" \"#  \"m m\"  #\"  #")
+		fmt.Fprintln(os.Stderr, "#     #   #   #m#   #\"\"\"\"")
+		fmt.Fprintln(os.Stderr, "#     \"#m#\"    #    \"#mm\"")
+		fmt.Fprintf(os.Stderr, "\n")
+		fmt.Fprintln(os.Stderr, "Rove is an asychronous nomadic game about exploring a planet as part of a loose community.")
+		fmt.Fprintln(os.Stderr, "Visit https://mdiluz.github.io/rove/ for more information.")
+		fmt.Fprintf(os.Stderr, "\n")
 		printUsage()
 		os.Exit(1)
 	}
diff --git a/cmd/rove/main_test.go b/cmd/rove/main_test.go
index dd588e1..250988a 100644
--- a/cmd/rove/main_test.go
+++ b/cmd/rove/main_test.go
@@ -13,6 +13,7 @@ import (
 )
 
 func Test_InnerMain(t *testing.T) {
+	os.Setenv("NO_TLS", "1")
 
 	// Use temporary local user data
 	tmp, err := ioutil.TempDir(os.TempDir(), "rove-")
@@ -50,12 +51,17 @@ func Test_InnerMain(t *testing.T) {
 	assert.Error(t, InnerMain("command"))
 
 	// Give it commands
-	assert.NoError(t, InnerMain("command", "move", "N"))
+	assert.NoError(t, InnerMain("command", "toggle"))
 	assert.NoError(t, InnerMain("command", "stash"))
 	assert.NoError(t, InnerMain("command", "repair"))
+	assert.NoError(t, InnerMain("command", "upgrade", "capacity"))
 	assert.NoError(t, InnerMain("command", "broadcast", "abc"))
+	assert.NoError(t, InnerMain("command", "wait", "10"))
+	assert.NoError(t, InnerMain("command", "wait", "1", "turn", "NW", "toggle", "broadcast", "zyx"))
 
 	// Give it malformed commands
-	assert.Error(t, InnerMain("command", "move", "stash"))
+	assert.Error(t, InnerMain("command", "unknown"))
 	assert.Error(t, InnerMain("command", "broadcast"))
+	assert.Error(t, InnerMain("command", "upgrade"))
+	assert.Error(t, InnerMain("command", "1"))
 }
diff --git a/docker-compose-test.yml b/docker-compose-test.yml
new file mode 100644
index 0000000..aec8cee
--- /dev/null
+++ b/docker-compose-test.yml
@@ -0,0 +1,32 @@
+version: '3'
+
+services:
+  rove-test-server:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    image: rove:latest
+    ports:
+      - "9090:9090"
+    environment:
+      - PORT=9090
+      - DATA_PATH=/tmp/
+      - WORDS_FILE=data/words_alpha.txt
+      - TICK_RATE=10
+      - NO_TLS=1
+    command: [ "./rove-server"]
+
+  rove-tests:
+    depends_on: [ rove-test-server ]
+    build:
+      context: .
+      dockerfile: Dockerfile
+    image: rove:latest
+    environment:
+      - ROVE_GRPC=rove-test-server
+    command: [ "./script/wait-for-it.sh", "rove-test-server:9090", "--", "go", "test", "-v", "./...", "--tags=integration", "-cover", "-coverprofile=/mnt/coverage-data/c.out", "-count", "1" ]
+    volumes:
+      - /tmp/coverage-data:/mnt/coverage-data:rw
+
+
+  
diff --git a/docker-compose.yml b/docker-compose.yml
index 71013b5..d5606f7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,22 +15,11 @@ services:
       - PORT=9090
       - DATA_PATH=/mnt/rove-server
       - WORDS_FILE=data/words_alpha.txt
-      - TICK_RATE=5
+      - TICK_RATE=3
+      - CERT_NAME=${CERT_NAME}
     volumes:
       - persistent-data:/mnt/rove-server:rw
+      - /etc/letsencrypt/:/etc/letsencrypt/
     command: [ "./rove-server"]
 
-  rove-tests:
-    depends_on: [ rove-server ]
-    build:
-      context: .
-      dockerfile: Dockerfile
-    image: rove:latest
-    environment:
-      - ROVE_GRPC=rove-server
-    command: [ "./script/wait-for-it.sh", "rove-server:9090", "--", "go", "test", "-v", "./...", "--tags=integration", "-cover", "-coverprofile=/mnt/coverage-data/c.out", "-count", "1" ]
-    volumes:
-      - /tmp/coverage-data:/mnt/coverage-data:rw
-
-
   
diff --git a/cmd/rove-server/internal/accounts.go b/pkg/accounts/accounts.go
similarity index 91%
rename from cmd/rove-server/internal/accounts.go
rename to pkg/accounts/accounts.go
index 99362ad..eb8637d 100644
--- a/cmd/rove-server/internal/accounts.go
+++ b/pkg/accounts/accounts.go
@@ -1,4 +1,4 @@
-package internal
+package accounts
 
 // Accountant decribes something that stores accounts and account values
 type Accountant interface {
@@ -21,8 +21,8 @@ type Accountant interface {
 // Account represents a registered user
 type Account struct {
 	// Name simply describes the account and must be unique
-	Name string `json:"name"`
+	Name string
 
 	// Data represents internal account data
-	Data map[string]string `json:"data"`
+	Data map[string]string
 }
diff --git a/cmd/rove-server/internal/accounts_test.go b/pkg/accounts/accounts_test.go
similarity index 98%
rename from cmd/rove-server/internal/accounts_test.go
rename to pkg/accounts/accounts_test.go
index 9e7891f..bd2416f 100644
--- a/cmd/rove-server/internal/accounts_test.go
+++ b/pkg/accounts/accounts_test.go
@@ -1,4 +1,4 @@
-package internal
+package accounts
 
 import (
 	"testing"
diff --git a/cmd/rove-server/internal/simpleAccountant.go b/pkg/accounts/simpleAccountant.go
similarity index 97%
rename from cmd/rove-server/internal/simpleAccountant.go
rename to pkg/accounts/simpleAccountant.go
index 3168f30..9d6c43f 100644
--- a/cmd/rove-server/internal/simpleAccountant.go
+++ b/pkg/accounts/simpleAccountant.go
@@ -1,4 +1,4 @@
-package internal
+package accounts
 
 import (
 	"fmt"
@@ -9,7 +9,7 @@ import (
 
 // SimpleAccountant manages a set of accounts
 type SimpleAccountant struct {
-	Accounts map[string]Account `json:"accounts"`
+	Accounts map[string]Account
 }
 
 // NewSimpleAccountant creates a new accountant
diff --git a/pkg/maths/vector.go b/pkg/maths/vector.go
index 8a63708..a74ff16 100644
--- a/pkg/maths/vector.go
+++ b/pkg/maths/vector.go
@@ -8,8 +8,8 @@ import (
 
 // Vector desribes a 3D vector
 type Vector struct {
-	X int `json:"x"`
-	Y int `json:"y"`
+	X int
+	Y int
 }
 
 // Add adds one vector to another
@@ -89,13 +89,31 @@ func BearingToVector(b roveapi.Bearing) Vector {
 	switch b {
 	case roveapi.Bearing_North:
 		return Vector{Y: 1}
+	case roveapi.Bearing_NorthEast:
+		return Vector{X: 1, Y: 1}
 	case roveapi.Bearing_East:
 		return Vector{X: 1}
+	case roveapi.Bearing_SouthEast:
+		return Vector{X: 1, Y: -1}
 	case roveapi.Bearing_South:
 		return Vector{Y: -1}
+	case roveapi.Bearing_SouthWest:
+		return Vector{X: -1, Y: -1}
 	case roveapi.Bearing_West:
 		return Vector{X: -1}
+	case roveapi.Bearing_NorthWest:
+		return Vector{X: -1, Y: 1}
 	}
 
 	return Vector{}
 }
+
+// Dot returns the dot product of two vectors
+func Dot(a Vector, b Vector) int {
+	return a.X*b.X + a.Y*b.Y
+}
+
+// AngleCos returns the cosine of the angle between two vectors
+func AngleCos(a Vector, b Vector) float64 {
+	return float64(Dot(a, b)) / a.Length() * b.Length()
+}
diff --git a/pkg/atlas/atlas.go b/pkg/rove/atlas.go
similarity index 97%
rename from pkg/atlas/atlas.go
rename to pkg/rove/atlas.go
index 05012aa..ab09dba 100644
--- a/pkg/atlas/atlas.go
+++ b/pkg/rove/atlas.go
@@ -1,4 +1,4 @@
-package atlas
+package rove
 
 import (
 	"github.com/mdiluz/rove/pkg/maths"
diff --git a/pkg/atlas/atlas_test.go b/pkg/rove/atlas_test.go
similarity index 99%
rename from pkg/atlas/atlas_test.go
rename to pkg/rove/atlas_test.go
index 3d715f1..56e5d8d 100644
--- a/pkg/atlas/atlas_test.go
+++ b/pkg/rove/atlas_test.go
@@ -1,4 +1,4 @@
-package atlas
+package rove
 
 import (
 	"testing"
diff --git a/pkg/atlas/chunkAtlas.go b/pkg/rove/chunkAtlas.go
similarity index 74%
rename from pkg/atlas/chunkAtlas.go
rename to pkg/rove/chunkAtlas.go
index 3f19b07..1903c7b 100644
--- a/pkg/atlas/chunkAtlas.go
+++ b/pkg/rove/chunkAtlas.go
@@ -1,62 +1,54 @@
-package atlas
+package rove
 
 import (
 	"log"
-	"math/rand"
 
 	"github.com/mdiluz/rove/pkg/maths"
 	"github.com/mdiluz/rove/proto/roveapi"
-	"github.com/ojrac/opensimplex-go"
 )
 
 // chunk represents a fixed square grid of tiles
 type chunk struct {
 	// Tiles represents the tiles within the chunk
-	Tiles []byte `json:"tiles"`
+	Tiles []byte
 
 	// Objects represents the objects within the chunk
 	// only one possible object per tile for now
-	Objects map[int]Object `json:"objects"`
+	Objects map[int]Object
 }
 
 // chunkBasedAtlas represents a grid of Chunks
 type chunkBasedAtlas struct {
 	// Chunks represents all chunks in the world
 	// This is intentionally not a 2D array so it can be expanded in all directions
-	Chunks []chunk `json:"chunks"`
+	Chunks []chunk
 
 	// LowerBound is the origin of the bottom left corner of the current chunks in world space (current chunks cover >= this value)
-	LowerBound maths.Vector `json:"lowerBound"`
+	LowerBound maths.Vector
 
 	// UpperBound is the top left corner of the current chunks (curent chunks cover < this value)
-	UpperBound maths.Vector `json:"upperBound"`
+	UpperBound maths.Vector
 
 	// ChunkSize is the x/y dimensions of each square chunk
-	ChunkSize int `json:"chunksize"`
+	ChunkSize int
 
-	// terrainNoise describes the noise function for the terrain
-	terrainNoise opensimplex.Noise
-
-	// terrainNoise describes the noise function for the terrain
-	objectNoise opensimplex.Noise
+	// worldGen is the internal world generator
+	worldGen WorldGen
 }
 
 const (
-	noiseSeed         = 1024
-	terrainNoiseScale = 6
-	objectNoiseScale  = 3
+	noiseSeed = 1024
 )
 
 // NewChunkAtlas creates a new empty atlas
 func NewChunkAtlas(chunkSize int) Atlas {
 	// Start up with one chunk
 	a := chunkBasedAtlas{
-		ChunkSize:    chunkSize,
-		Chunks:       make([]chunk, 1),
-		LowerBound:   maths.Vector{X: 0, Y: 0},
-		UpperBound:   maths.Vector{X: chunkSize, Y: chunkSize},
-		terrainNoise: opensimplex.New(noiseSeed),
-		objectNoise:  opensimplex.New(noiseSeed),
+		ChunkSize:  chunkSize,
+		Chunks:     make([]chunk, 1),
+		LowerBound: maths.Vector{X: 0, Y: 0},
+		UpperBound: maths.Vector{X: chunkSize, Y: chunkSize},
+		worldGen:   NewNoiseWorldGen(noiseSeed),
 	}
 	// Initialise the first chunk
 	a.populate(0)
@@ -105,41 +97,16 @@ func (a *chunkBasedAtlas) populate(chunk int) {
 	origin := a.chunkOriginInWorldSpace(chunk)
 	for i := 0; i < a.ChunkSize; i++ {
 		for j := 0; j < a.ChunkSize; j++ {
+			loc := maths.Vector{X: origin.X + i, Y: origin.Y + j}
 
-			// Get the terrain noise value for this location
-			t := a.terrainNoise.Eval2(float64(origin.X+i)/terrainNoiseScale, float64(origin.Y+j)/terrainNoiseScale)
-			var tile roveapi.Tile
-			switch {
-			case t > 0.5:
-				tile = roveapi.Tile_Gravel
-			case t > 0.05:
-				tile = roveapi.Tile_Sand
-			default:
-				tile = roveapi.Tile_Rock
-			}
-			c.Tiles[j*a.ChunkSize+i] = byte(tile)
+			// Set the tile
+			c.Tiles[j*a.ChunkSize+i] = byte(a.worldGen.GetTile(loc))
 
-			// Get the object noise value for this location
-			o := a.objectNoise.Eval2(float64(origin.X+i)/objectNoiseScale, float64(origin.Y+j)/objectNoiseScale)
-			var obj = roveapi.Object_ObjectUnknown
-			switch {
-			case o > 0.6:
-				obj = roveapi.Object_RockLarge
-			case o > 0.5:
-				obj = roveapi.Object_RockSmall
+			// Set the object
+			obj := a.worldGen.GetObject(loc)
+			if obj.Type != roveapi.Object_ObjectUnknown {
+				c.Objects[j*a.ChunkSize+i] = obj
 			}
-			if obj != roveapi.Object_ObjectUnknown {
-				c.Objects[j*a.ChunkSize+i] = Object{Type: roveapi.Object(obj)}
-			}
-		}
-	}
-
-	// Set up any objects
-	for i := 0; i < len(c.Tiles); i++ {
-		if rand.Intn(16) == 0 {
-			c.Objects[i] = Object{Type: roveapi.Object_RockLarge}
-		} else if rand.Intn(32) == 0 {
-			c.Objects[i] = Object{Type: roveapi.Object_RockSmall}
 		}
 	}
 
@@ -236,12 +203,11 @@ func (a *chunkBasedAtlas) worldSpaceToChunkWithGrow(v maths.Vector) int {
 
 	// Create the new empty atlas
 	newAtlas := chunkBasedAtlas{
-		ChunkSize:    a.ChunkSize,
-		LowerBound:   lower,
-		UpperBound:   upper,
-		Chunks:       make([]chunk, size.X*size.Y),
-		terrainNoise: a.terrainNoise,
-		objectNoise:  a.objectNoise,
+		ChunkSize:  a.ChunkSize,
+		LowerBound: lower,
+		UpperBound: upper,
+		Chunks:     make([]chunk, size.X*size.Y),
+		worldGen:   a.worldGen,
 	}
 
 	// Log that we're resizing
diff --git a/pkg/rove/command.go b/pkg/rove/command.go
deleted file mode 100644
index e64d646..0000000
--- a/pkg/rove/command.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package rove
-
-import "github.com/mdiluz/rove/proto/roveapi"
-
-// Command represends a single command to execute
-type Command struct {
-	Command roveapi.CommandType `json:"command"`
-
-	// Used in the move command
-	Bearing roveapi.Bearing `json:"bearing,omitempty"`
-
-	// Used in the broadcast command
-	Message []byte `json:"message,omitempty"`
-}
-
-// CommandStream is a list of commands to execute in order
-type CommandStream []Command
diff --git a/pkg/rove/command_test.go b/pkg/rove/command_test.go
index 337782f..bf1eed6 100644
--- a/pkg/rove/command_test.go
+++ b/pkg/rove/command_test.go
@@ -1,6 +1,7 @@
 package rove
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/mdiluz/rove/pkg/maths"
@@ -8,63 +9,297 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestCommand_Move(t *testing.T) {
-	world := NewWorld(8)
-	a, err := world.SpawnRover()
+func TestCommand_Invalid(t *testing.T) {
+	w := NewWorld(8)
+	name, err := w.SpawnRover("")
 	assert.NoError(t, err)
-	pos := maths.Vector{
-		X: 1.0,
-		Y: 2.0,
-	}
 
-	err = world.WarpRover(a, pos)
-	assert.NoError(t, err, "Failed to set position for rover")
-
-	// Try the move command
-	moveCommand := Command{Command: roveapi.CommandType_move, Bearing: roveapi.Bearing_North}
-	assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to execute move command")
-
-	// Tick the world
-	world.EnqueueAllIncoming()
-	world.ExecuteCommandQueues()
-
-	newPos, err := world.RoverPosition(a)
-	assert.NoError(t, err, "Failed to set position for rover")
-	pos.Add(maths.Vector{X: 0.0, Y: 1})
-	assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_none})
+	assert.Error(t, err)
 }
 
-func TestCommand_Recharge(t *testing.T) {
-	world := NewWorld(8)
-	a, err := world.SpawnRover()
+func TestCommand_Toggle(t *testing.T) {
+	w := NewWorld(8)
+	a, err := w.SpawnRover("")
 	assert.NoError(t, err)
-	pos := maths.Vector{
-		X: 1.0,
-		Y: 2.0,
+
+	r, err := w.GetRover(a)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
+
+	err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_toggle})
+	assert.NoError(t, err)
+	w.Tick()
+
+	r, err = w.GetRover(a)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.SailPosition_CatchingWind, r.SailPosition)
+
+	err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_toggle})
+	assert.NoError(t, err)
+	w.Tick()
+
+	r, err = w.GetRover(a)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
+}
+
+func TestCommand_Turn(t *testing.T) {
+	w := NewWorld(8)
+	a, err := w.SpawnRover("")
+	assert.NoError(t, err)
+
+	err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_turn, Bearing: roveapi.Bearing_NorthWest})
+	assert.NoError(t, err)
+	w.Tick()
+
+	r, err := w.GetRover(a)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.Bearing_NorthWest, r.Bearing)
+}
+
+func TestCommand_Stash(t *testing.T) {
+	w := NewWorld(8)
+	name, err := w.SpawnRover("")
+	assert.NoError(t, err)
+
+	info, err := w.GetRover(name)
+	assert.NoError(t, err)
+	assert.Empty(t, info.Inventory)
+
+	// Drop a pickup below us
+	w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RockSmall})
+
+	// Try and stash it
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_stash})
+	assert.NoError(t, err)
+	w.Tick()
+
+	// Check we now have it in the inventory
+	info, err = w.GetRover(name)
+	assert.NoError(t, err)
+	assert.Equal(t, 1, len(info.Inventory))
+	assert.Equal(t, Object{Type: roveapi.Object_RockSmall}, info.Inventory[0])
+
+	// Check it's no longer on the atlas
+	_, obj := w.Atlas.QueryPosition(info.Pos)
+	assert.Equal(t, Object{Type: roveapi.Object_ObjectUnknown}, obj)
+}
+
+func TestCommand_Repair(t *testing.T) {
+	w := NewWorld(8)
+	name, err := w.SpawnRover("")
+	assert.NoError(t, err)
+
+	info, err := w.GetRover(name)
+	assert.NoError(t, err)
+	assert.Equal(t, info.MaximumIntegrity, info.Integrity)
+
+	// Put a blocking rock to the north
+	w.Atlas.SetObject(info.Pos.Added(maths.Vector{X: 0, Y: 1}), Object{Type: roveapi.Object_RockLarge})
+
+	// Try and move and make sure we're blocked
+	newpos, err := w.TryMoveRover(name, roveapi.Bearing_North)
+	assert.NoError(t, err)
+	assert.Equal(t, info.Pos, newpos)
+
+	// Check we're damaged
+	info, err = w.GetRover(name)
+	assert.NoError(t, err)
+	assert.Equal(t, info.MaximumIntegrity-1, info.Integrity)
+
+	// Stash a repair object
+	w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RoverParts})
+	obj, err := w.RoverStash(name)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.Object_RoverParts, obj)
+
+	// Enqueue the repair and tick
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_repair})
+	assert.NoError(t, err)
+	w.Tick()
+
+	// Check we're repaired
+	info, err = w.GetRover(name)
+	assert.NoError(t, err)
+	assert.Equal(t, info.MaximumIntegrity, info.Integrity)
+	assert.Equal(t, 0, len(info.Inventory))
+}
+
+func TestCommand_Broadcast(t *testing.T) {
+	w := NewWorld(8)
+	name, err := w.SpawnRover("")
+	assert.NoError(t, err)
+
+	// Enqueue the broadcast and tick
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_broadcast, Data: []byte("ABC")})
+	assert.NoError(t, err)
+	w.Tick()
+
+	info, err := w.GetRover(name)
+	assert.NoError(t, err)
+	assert.Contains(t, info.Logs[len(info.Logs)-1].Text, "ABC")
+}
+
+func TestCommand_Salvage(t *testing.T) {
+	w := NewWorld(8)
+	name, err := w.SpawnRover("")
+	assert.NoError(t, err)
+
+	info, err := w.GetRover(name)
+	assert.NoError(t, err)
+
+	w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RoverDormant})
+
+	// Enqueue the broadcast and tick
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_salvage})
+	assert.NoError(t, err)
+	w.Tick()
+
+	// Check we now have some rover parts
+	info, err = w.GetRover(name)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, info.Inventory)
+	for _, i := range info.Inventory {
+		assert.Equal(t, roveapi.Object_RoverParts, i.Type)
 	}
 
-	err = world.WarpRover(a, pos)
-	assert.NoError(t, err, "Failed to set position for rover")
+	// Check the dormant rover is gone
+	_, obj := w.Atlas.QueryPosition(info.Pos)
+	assert.Equal(t, roveapi.Object_ObjectUnknown, obj.Type)
+}
 
-	// Move to use up some charge
-	moveCommand := Command{Command: roveapi.CommandType_move, Bearing: roveapi.Bearing_North}
-	assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to queue move command")
+func TestCommand_Transfer(t *testing.T) {
+	w := NewWorld(8)
+	acc, err := w.Accountant.RegisterAccount("tmp")
+	assert.NoError(t, err)
+	nameA, err := w.SpawnRover(acc.Name)
+	assert.NoError(t, err)
 
-	// Tick the world
-	world.EnqueueAllIncoming()
-	world.ExecuteCommandQueues()
+	infoA, err := w.GetRover(nameA)
+	assert.NoError(t, err)
 
-	rover, _ := world.GetRover(a)
-	assert.Equal(t, rover.MaximumCharge-1, rover.Charge)
+	// Drop a dormant rover on the current position
+	infoB := DefaultRover()
+	infoB.Name = "abc"
+	infoB.Pos = infoA.Pos
+	data, err := json.Marshal(infoB)
+	assert.NoError(t, err)
+	w.Atlas.SetObject(infoA.Pos, Object{Type: roveapi.Object_RoverDormant, Data: data})
 
-	chargeCommand := Command{Command: roveapi.CommandType_recharge}
-	assert.NoError(t, world.Enqueue(a, chargeCommand), "Failed to queue recharge command")
+	// Enqueue a transfer as well as a dud command
+	err = w.Enqueue(nameA,
+		&roveapi.Command{Command: roveapi.CommandType_transfer},
+		&roveapi.Command{Command: roveapi.CommandType_broadcast, Data: []byte("xyz")})
+	assert.NoError(t, err)
+	w.Tick()
 
-	// Tick the world
-	world.EnqueueAllIncoming()
-	world.ExecuteCommandQueues()
+	// Ensure both command queues are empty
+	assert.Empty(t, w.CommandQueue[nameA])
+	assert.Empty(t, w.CommandQueue[infoB.Name])
 
-	rover, _ = world.GetRover(a)
-	assert.Equal(t, rover.MaximumCharge, rover.Charge)
+	// Verify the account now controls the new rover
+	accountRover, err := w.Accountant.GetValue(acc.Name, "rover")
+	assert.NoError(t, err)
+	assert.Equal(t, infoB.Name, accountRover)
+
+	// Verify the position now has a dormant rover
+	_, obj := w.Atlas.QueryPosition(infoA.Pos)
+	assert.Equal(t, roveapi.Object_RoverDormant, obj.Type)
+
+	// Verify the stored data matches
+	var stored Rover
+	err = json.Unmarshal(obj.Data, &stored)
+	assert.NoError(t, err)
+	assert.Equal(t, infoA.Name, stored.Name)
+
+	// Verify the new rover data matches what we put in
+	infoB2, err := w.GetRover(infoB.Name)
+	assert.NoError(t, err)
+	assert.Equal(t, infoB.Name, infoB2.Name)
 
 }
+
+func TestCommand_Wait(t *testing.T) {
+	w := NewWorld(8)
+	a, err := w.SpawnRover("")
+	assert.NoError(t, err)
+
+	r, err := w.GetRover(a)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
+
+	err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_wait, Repeat: 4}, &roveapi.Command{Command: roveapi.CommandType_toggle})
+	assert.NoError(t, err)
+
+	// Tick 5 times during the wait (1 normal execute + 4)
+	for i := 0; i < 5; i++ {
+		w.Tick()
+
+		r, err = w.GetRover(a)
+		assert.NoError(t, err)
+		assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
+	}
+
+	// One last tick to do the toggle
+	w.Tick()
+
+	r, err = w.GetRover(a)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.SailPosition_CatchingWind, r.SailPosition)
+}
+
+func TestCommand_Upgrade(t *testing.T) {
+	w := NewWorld(8)
+	name, err := w.SpawnRover("")
+	assert.NoError(t, err)
+	rover, ok := w.Rovers[name]
+	assert.True(t, ok)
+
+	// Try an invalid upgrade
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade})
+	assert.Error(t, err)
+
+	// Try a valid command but without the parts
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade, Upgrade: roveapi.RoverUpgrade_Capacity})
+	assert.NoError(t, err)
+
+	// Ensure nothing changed and we logged the attempt
+	pre := rover.Capacity
+	w.Tick()
+	assert.Equal(t, pre, rover.Capacity)
+	assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "tried")
+
+	// One non-part item
+	rover.Inventory = []Object{
+		{
+			Type: roveapi.Object_RoverParts,
+		},
+		{
+			Type: roveapi.Object_RoverParts,
+		},
+		{
+			Type: roveapi.Object_RockSmall,
+		},
+		{
+			Type: roveapi.Object_RoverParts,
+		},
+		{
+			Type: roveapi.Object_RoverParts,
+		},
+		{
+			Type: roveapi.Object_RoverParts,
+		},
+	}
+
+	// Try a valid command again
+	err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade, Upgrade: roveapi.RoverUpgrade_Capacity})
+	assert.NoError(t, err)
+
+	// Check that the capacity increases on the tick and all the parts are used
+	pre = rover.Capacity
+	w.Tick()
+	assert.Equal(t, pre+1, rover.Capacity)
+	assert.Equal(t, 1, len(rover.Inventory))
+	assert.Equal(t, roveapi.Object_RockSmall, rover.Inventory[0].Type)
+}
diff --git a/pkg/atlas/objects.go b/pkg/rove/objects.go
similarity index 82%
rename from pkg/atlas/objects.go
rename to pkg/rove/objects.go
index e1b6730..df10453 100644
--- a/pkg/atlas/objects.go
+++ b/pkg/rove/objects.go
@@ -1,4 +1,4 @@
-package atlas
+package rove
 
 import (
 	"github.com/mdiluz/rove/proto/roveapi"
@@ -7,7 +7,10 @@ import (
 // Object represents an object in the world
 type Object struct {
 	// The type of the object
-	Type roveapi.Object `json:"type"`
+	Type roveapi.Object
+
+	// Data is an internal type used for certain types of object
+	Data []byte
 }
 
 // IsBlocking checks if an object is a blocking object
@@ -29,6 +32,7 @@ func (o *Object) IsBlocking() bool {
 func (o *Object) IsStashable() bool {
 	var stashable = [...]roveapi.Object{
 		roveapi.Object_RockSmall,
+		roveapi.Object_RoverParts,
 	}
 
 	for _, t := range stashable {
diff --git a/pkg/rove/rover.go b/pkg/rove/rover.go
index 089b44a..bebde50 100644
--- a/pkg/rove/rover.go
+++ b/pkg/rove/rover.go
@@ -1,54 +1,89 @@
 package rove
 
 import (
+	"bufio"
 	"fmt"
 	"log"
+	"math/rand"
+	"os"
 	"time"
 
-	"github.com/mdiluz/rove/pkg/atlas"
+	"github.com/google/uuid"
 	"github.com/mdiluz/rove/pkg/maths"
+	"github.com/mdiluz/rove/proto/roveapi"
+)
+
+const (
+	maxLogEntries = 16
 )
 
 // RoverLogEntry describes a single log entry for the rover
 type RoverLogEntry struct {
 	// Time is the timestamp of the entry
-	Time time.Time `json:"time"`
+	Time time.Time
 
 	// Text contains the information in this log entry
-	Text string `json:"text"`
+	Text string
 }
 
 // Rover describes a single rover in the world
 type Rover struct {
 	// Unique name of this rover
-	Name string `json:"name"`
+	Name string
 
 	// Pos represents where this rover is in the world
-	Pos maths.Vector `json:"pos"`
+	Pos maths.Vector
+
+	// Bearing is the current direction the rover is facing
+	Bearing roveapi.Bearing
 
 	// Range represents the distance the unit's radar can see
-	Range int `json:"range"`
+	Range int
 
 	// Inventory represents any items the rover is carrying
-	Inventory []atlas.Object `json:"inventory"`
+	Inventory []Object
 
 	// Capacity is the maximum number of inventory items
-	Capacity int `json:"capacity"`
+	Capacity int
 
 	// Integrity represents current rover health
-	Integrity int `json:"integrity"`
+	Integrity int
 
 	// MaximumIntegrity is the full integrity of the rover
-	MaximumIntegrity int `json:"maximum-integrity"`
+	MaximumIntegrity int
 
 	// Charge is the amount of energy the rover has
-	Charge int `json:"charge"`
+	Charge int
 
 	// MaximumCharge is the maximum charge able to be stored
-	MaximumCharge int `json:"maximum-Charge"`
+	MaximumCharge int
+
+	// SailPosition is the current position of the sails
+	SailPosition roveapi.SailPosition
+
+	// Current number of ticks in this move, used for sailing speeds
+	MoveTicks int
 
 	// Logs Stores log of information
-	Logs []RoverLogEntry `json:"logs"`
+	Logs []RoverLogEntry
+
+	// The account that owns this rover
+	Owner string
+}
+
+// DefaultRover returns a default rover object with default settings
+func DefaultRover() *Rover {
+	return &Rover{
+		Range:            10,
+		Integrity:        10,
+		MaximumIntegrity: 10,
+		Capacity:         10,
+		Charge:           10,
+		MaximumCharge:    10,
+		Bearing:          roveapi.Bearing_North,
+		SailPosition:     roveapi.SailPosition_SolarCharging,
+		Name:             GenerateRoverName(),
+	}
 }
 
 // AddLogEntryf adds an entry to the rovers log
@@ -61,4 +96,42 @@ func (r *Rover) AddLogEntryf(format string, args ...interface{}) {
 			Text: text,
 		},
 	)
+
+	// Limit the number of logs
+	if len(r.Logs) > maxLogEntries {
+		r.Logs = r.Logs[len(r.Logs)-maxLogEntries:]
+	}
+}
+
+var wordsFile = os.Getenv("WORDS_FILE")
+var roverWords []string
+
+// GenerateRoverName generates a new rover name
+func GenerateRoverName() string {
+
+	// Try and load the rover words file
+	if len(roverWords) == 0 {
+		// Try and load the words file
+		if file, err := os.Open(wordsFile); err != nil {
+			log.Printf("Couldn't read words file [%s], running without words: %s\n", wordsFile, err)
+		} else {
+			defer file.Close()
+			scanner := bufio.NewScanner(file)
+			for scanner.Scan() {
+				roverWords = append(roverWords, scanner.Text())
+			}
+			if scanner.Err() != nil {
+				log.Printf("Failure during word file scan: %s\n", scanner.Err())
+			}
+		}
+	}
+
+	// Assign a random name if we have words
+	if len(roverWords) > 0 {
+		// Loop until we find a unique name
+		return fmt.Sprintf("%s-%s", roverWords[rand.Intn(len(roverWords))], roverWords[rand.Intn(len(roverWords))])
+	}
+
+	// Default to a unique string
+	return uuid.New().String()
 }
diff --git a/pkg/rove/world.go b/pkg/rove/world.go
index 7c313ba..d509d8d 100644
--- a/pkg/rove/world.go
+++ b/pkg/rove/world.go
@@ -1,104 +1,81 @@
 package rove
 
 import (
-	"bufio"
+	"encoding/json"
 	"fmt"
 	"log"
 	"math/rand"
-	"os"
 	"sync"
 
-	"github.com/google/uuid"
-	"github.com/mdiluz/rove/pkg/atlas"
+	"github.com/mdiluz/rove/pkg/accounts"
 	"github.com/mdiluz/rove/pkg/maths"
 	"github.com/mdiluz/rove/proto/roveapi"
 )
 
+const (
+	// ticksPerNormalMove defines the number of ticks it should take for a "normal" speed move
+	ticksPerNormalMove = 4
+
+	// upgradeCost is the cost in rover parts needed to upgrade a rover specification
+	upgradeCost = 5
+)
+
+// CommandStream is a list of commands to execute in order
+type CommandStream []*roveapi.Command
+
 // World describes a self contained universe and everything in it
 type World struct {
+
 	// TicksPerDay is the amount of ticks in a single day
-	TicksPerDay int `json:"ticks-per-day"`
+	TicksPerDay int
 
 	// Current number of ticks from the start
-	CurrentTicks int `json:"current-ticks"`
+	CurrentTicks int
 
 	// Rovers is a id->data map of all the rovers in the game
-	Rovers map[string]Rover `json:"rovers"`
+	Rovers map[string]*Rover
 
 	// Atlas represends the world map of chunks and tiles
-	Atlas atlas.Atlas `json:"atlas"`
+	Atlas Atlas
+
+	// Wind is the current wind direction
+	Wind roveapi.Bearing
 
 	// Commands is the set of currently executing command streams per rover
-	CommandQueue map[string]CommandStream `json:"commands"`
-	// Incoming represents the set of commands to add to the queue at the end of the current tick
-	CommandIncoming map[string]CommandStream `json:"incoming"`
+	CommandQueue map[string]CommandStream
+
+	// Accountant
+	Accountant accounts.Accountant
 
 	// Mutex to lock around all world operations
 	worldMutex sync.RWMutex
 	// Mutex to lock around command operations
 	cmdMutex sync.RWMutex
-	// Set of possible words to use for names
-	words []string
 }
 
-var wordsFile = os.Getenv("WORDS_FILE")
-
 // NewWorld creates a new world object
 func NewWorld(chunkSize int) *World {
-
-	// Try and load the words file
-	var lines []string
-	if file, err := os.Open(wordsFile); err != nil {
-		log.Printf("Couldn't read words file [%s], running without words: %s\n", wordsFile, err)
-	} else {
-		defer file.Close()
-		scanner := bufio.NewScanner(file)
-		for scanner.Scan() {
-			lines = append(lines, scanner.Text())
-		}
-		if scanner.Err() != nil {
-			log.Printf("Failure during word file scan: %s\n", scanner.Err())
-		}
-	}
-
 	return &World{
-		Rovers:          make(map[string]Rover),
-		CommandQueue:    make(map[string]CommandStream),
-		CommandIncoming: make(map[string]CommandStream),
-		Atlas:           atlas.NewChunkAtlas(chunkSize),
-		words:           lines,
-		TicksPerDay:     24,
-		CurrentTicks:    0,
+		Rovers:       make(map[string]*Rover),
+		CommandQueue: make(map[string]CommandStream),
+		Atlas:        NewChunkAtlas(chunkSize),
+		TicksPerDay:  24,
+		CurrentTicks: 0,
+		Accountant:   accounts.NewSimpleAccountant(),
+		Wind:         roveapi.Bearing_North,
 	}
 }
 
-// SpawnRover adds an rover to the game
-func (w *World) SpawnRover() (string, error) {
+// SpawnRover adds an rover to the game (without lock)
+func (w *World) SpawnRover(account string) (string, error) {
 	w.worldMutex.Lock()
 	defer w.worldMutex.Unlock()
 
 	// Initialise the rover
-	rover := Rover{
-		Range:            4,
-		Integrity:        10,
-		MaximumIntegrity: 10,
-		Capacity:         10,
-		Charge:           10,
-		MaximumCharge:    10,
-		Name:             uuid.New().String(),
-	}
+	rover := DefaultRover()
 
-	// Assign a random name if we have words
-	if len(w.words) > 0 {
-		for {
-			// Loop until we find a unique name
-			name := fmt.Sprintf("%s-%s", w.words[rand.Intn(len(w.words))], w.words[rand.Intn(len(w.words))])
-			if _, ok := w.Rovers[name]; !ok {
-				rover.Name = name
-				break
-			}
-		}
-	}
+	// Assign the owner
+	rover.Owner = account
 
 	// Spawn in a random place near the origin
 	rover.Pos = maths.Vector{
@@ -124,7 +101,13 @@ func (w *World) SpawnRover() (string, error) {
 	// Append the rover to the list
 	w.Rovers[rover.Name] = rover
 
-	return rover.Name, nil
+	var err error
+	// Only assign if we've been given an account
+	if len(account) > 0 {
+		err = w.Accountant.AssignData(account, "rover", rover.Name)
+	}
+
+	return rover.Name, err
 }
 
 // GetRover gets a specific rover by name
@@ -136,7 +119,7 @@ func (w *World) GetRover(rover string) (Rover, error) {
 	if !ok {
 		return Rover{}, fmt.Errorf("Failed to find rover with name: %s", rover)
 	}
-	return i, nil
+	return *i, nil
 }
 
 // RoverRecharge charges up a rover
@@ -159,7 +142,6 @@ func (w *World) RoverRecharge(rover string) (int, error) {
 		i.Charge++
 		i.AddLogEntryf("recharged to %d", i.Charge)
 	}
-	w.Rovers[rover] = i
 
 	return i.Charge, nil
 }
@@ -194,7 +176,6 @@ func (w *World) RoverBroadcast(rover string, message []byte) (err error) {
 	}
 
 	i.AddLogEntryf("broadcasted %s", string(message))
-	w.Rovers[rover] = i
 	return
 }
 
@@ -203,12 +184,26 @@ func (w *World) DestroyRover(rover string) error {
 	w.worldMutex.Lock()
 	defer w.worldMutex.Unlock()
 
-	_, ok := w.Rovers[rover]
+	r, ok := w.Rovers[rover]
 	if !ok {
 		return fmt.Errorf("no rover matching id")
 	}
 
+	// Remove this rover from tracked rovers
 	delete(w.Rovers, rover)
+
+	r.Owner = ""
+	r.AddLogEntryf("rover destroyed")
+
+	// Marshal the rover data
+	data, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+
+	// Place the dormant rover down
+	w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_RoverDormant, Data: data})
+
 	return nil
 }
 
@@ -235,12 +230,11 @@ func (w *World) SetRoverPosition(rover string, pos maths.Vector) error {
 	}
 
 	i.Pos = pos
-	w.Rovers[rover] = i
 	return nil
 }
 
 // RoverInventory returns the inventory of a requested rover
-func (w *World) RoverInventory(rover string) ([]atlas.Object, error) {
+func (w *World) RoverInventory(rover string) ([]Object, error) {
 	w.worldMutex.RLock()
 	defer w.worldMutex.RUnlock()
 
@@ -272,12 +266,11 @@ func (w *World) WarpRover(rover string, pos maths.Vector) error {
 	}
 
 	i.Pos = pos
-	w.Rovers[rover] = i
 	return nil
 }
 
-// MoveRover attempts to move a rover in a specific direction
-func (w *World) MoveRover(rover string, b roveapi.Bearing) (maths.Vector, error) {
+// TryMoveRover attempts to move a rover in a specific direction
+func (w *World) TryMoveRover(rover string, b roveapi.Bearing) (maths.Vector, error) {
 	w.worldMutex.Lock()
 	defer w.worldMutex.Unlock()
 
@@ -286,12 +279,6 @@ func (w *World) MoveRover(rover string, b roveapi.Bearing) (maths.Vector, error)
 		return maths.Vector{}, fmt.Errorf("no rover matching id")
 	}
 
-	// Ensure the rover has energy
-	if i.Charge <= 0 {
-		return i.Pos, nil
-	}
-	i.Charge--
-
 	// Try the new move position
 	newPos := i.Pos.Added(maths.BearingToVector(b))
 
@@ -301,17 +288,11 @@ func (w *World) MoveRover(rover string, b roveapi.Bearing) (maths.Vector, error)
 		i.AddLogEntryf("moved %s to %+v", b.String(), newPos)
 		// Perform the move
 		i.Pos = newPos
-		w.Rovers[rover] = i
 	} else {
 		// If it is a blocking tile, reduce the rover integrity
 		i.AddLogEntryf("tried to move %s to %+v", b.String(), newPos)
 		i.Integrity = i.Integrity - 1
 		i.AddLogEntryf("had a collision, new integrity %d", i.Integrity)
-		if i.Integrity == 0 {
-			// TODO: The rover needs to be left dormant with the player
-		} else {
-			w.Rovers[rover] = i
-		}
 	}
 
 	return i.Pos, nil
@@ -329,11 +310,13 @@ func (w *World) RoverStash(rover string) (roveapi.Object, error) {
 
 	// Can't pick up when full
 	if len(r.Inventory) >= r.Capacity {
+		r.AddLogEntryf("tried to stash object but inventory was full")
 		return roveapi.Object_ObjectUnknown, nil
 	}
 
 	// Ensure the rover has energy
 	if r.Charge <= 0 {
+		r.AddLogEntryf("tried to stash object but had no charge")
 		return roveapi.Object_ObjectUnknown, nil
 	}
 	r.Charge--
@@ -345,11 +328,237 @@ func (w *World) RoverStash(rover string) (roveapi.Object, error) {
 
 	r.AddLogEntryf("stashed %c", obj.Type)
 	r.Inventory = append(r.Inventory, obj)
-	w.Rovers[rover] = r
-	w.Atlas.SetObject(r.Pos, atlas.Object{Type: roveapi.Object_ObjectUnknown})
+	w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_ObjectUnknown})
 	return obj.Type, nil
 }
 
+// RoverSalvage will salvage a rover for parts
+func (w *World) RoverSalvage(rover string) (roveapi.Object, error) {
+	w.worldMutex.Lock()
+	defer w.worldMutex.Unlock()
+
+	r, ok := w.Rovers[rover]
+	if !ok {
+		return roveapi.Object_ObjectUnknown, fmt.Errorf("no rover matching id")
+	}
+
+	// Can't pick up when full
+	if len(r.Inventory) >= r.Capacity {
+		r.AddLogEntryf("tried to salvage dormant rover but inventory was full")
+		return roveapi.Object_ObjectUnknown, nil
+	}
+
+	// Ensure the rover has energy
+	if r.Charge <= 0 {
+		r.AddLogEntryf("tried to salvage dormant rover but had no charge")
+		return roveapi.Object_ObjectUnknown, nil
+	}
+	r.Charge--
+
+	_, obj := w.Atlas.QueryPosition(r.Pos)
+	if obj.Type != roveapi.Object_RoverDormant {
+		r.AddLogEntryf("tried to salvage dormant rover but found no rover to salvage")
+		return roveapi.Object_ObjectUnknown, nil
+	}
+
+	r.AddLogEntryf("salvaged dormant rover")
+	for i := 0; i < 5; i++ {
+		if len(r.Inventory) == r.Capacity {
+			break
+		}
+		r.Inventory = append(r.Inventory, Object{Type: roveapi.Object_RoverParts})
+	}
+	w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_ObjectUnknown})
+	return obj.Type, nil
+}
+
+// RoverTransfer will transfer rover control to dormant rover
+func (w *World) RoverTransfer(rover string) (string, error) {
+	w.worldMutex.Lock()
+	defer w.worldMutex.Unlock()
+
+	oldRover, ok := w.Rovers[rover]
+	if !ok {
+		return "", fmt.Errorf("no rover matching id")
+	}
+
+	_, obj := w.Atlas.QueryPosition(oldRover.Pos)
+	if obj.Type != roveapi.Object_RoverDormant {
+		oldRover.AddLogEntryf("tried to transfer to dormant rover but found no rover")
+		return "", nil
+	}
+
+	// Unmarshal the dormant rover
+	var newRover Rover
+	err := json.Unmarshal(obj.Data, &newRover)
+	if err != nil {
+		return "", err
+	}
+
+	// Add logs
+	oldRover.AddLogEntryf("transferring to dormant rover %s", newRover.Name)
+	newRover.AddLogEntryf("transferred from rover %s", oldRover.Name)
+
+	// Transfer the ownership
+	err = w.Accountant.AssignData(oldRover.Owner, "rover", newRover.Name)
+	if err != nil {
+		return "", err
+	}
+	newRover.Owner = oldRover.Owner
+	oldRover.Owner = ""
+
+	// Place the old rover in the world
+	oldRoverData, err := json.Marshal(oldRover)
+	if err != nil {
+		return "", err
+	}
+	w.Atlas.SetObject(oldRover.Pos, Object{Type: roveapi.Object_RoverDormant, Data: oldRoverData})
+
+	// Swap the rovers in the tracking
+	w.Rovers[newRover.Name] = &newRover
+	delete(w.Rovers, oldRover.Name)
+
+	// Clear the command queues for both rovers
+	delete(w.CommandQueue, oldRover.Name)
+	delete(w.CommandQueue, newRover.Name)
+
+	return newRover.Name, nil
+}
+
+// RoverToggle will toggle the sail position
+func (w *World) RoverToggle(rover string) (roveapi.SailPosition, error) {
+	w.worldMutex.Lock()
+	defer w.worldMutex.Unlock()
+
+	r, ok := w.Rovers[rover]
+	if !ok {
+		return roveapi.SailPosition_UnknownSailPosition, fmt.Errorf("no rover matching id")
+	}
+
+	// Swap the sail position
+	switch r.SailPosition {
+	case roveapi.SailPosition_CatchingWind:
+		r.SailPosition = roveapi.SailPosition_SolarCharging
+	case roveapi.SailPosition_SolarCharging:
+		r.SailPosition = roveapi.SailPosition_CatchingWind
+	}
+
+	// Reset the movement ticks
+	r.MoveTicks = 0
+
+	return r.SailPosition, nil
+}
+
+// RoverUpgrade will try to upgrade the rover
+func (w *World) RoverUpgrade(rover string, upgrade roveapi.RoverUpgrade) (int, error) {
+	w.worldMutex.Lock()
+	defer w.worldMutex.Unlock()
+
+	r, ok := w.Rovers[rover]
+	if !ok {
+		return 0, fmt.Errorf("no rover matching id")
+	}
+
+	cost := upgradeCost
+	num := 0
+	for i := range r.Inventory {
+		if r.Inventory[i].Type == roveapi.Object_RoverParts {
+			num++
+		}
+	}
+
+	if num < cost {
+		r.AddLogEntryf("tried to upgrade but lacked rover parts")
+		return 0, nil
+	}
+
+	// Apply the upgrade
+	var ret int
+	switch upgrade {
+	case roveapi.RoverUpgrade_Capacity:
+		r.Capacity++
+		ret = r.Capacity
+	case roveapi.RoverUpgrade_Range:
+		r.Range++
+		ret = r.Range
+	case roveapi.RoverUpgrade_MaximumCharge:
+		r.MaximumCharge++
+		ret = r.MaximumCharge
+	case roveapi.RoverUpgrade_MaximumIntegrity:
+		r.MaximumIntegrity++
+		ret = r.MaximumIntegrity
+	default:
+		return 0, fmt.Errorf("unknown upgrade: %s", upgrade)
+	}
+
+	// Remove the cost in rover parts
+	var n []Object
+	for _, o := range r.Inventory {
+		if o.Type == roveapi.Object_RoverParts && cost > 0 {
+			cost--
+		} else {
+			n = append(n, o)
+		}
+	}
+	// Assign back the inventory
+	r.Inventory = n
+
+	r.AddLogEntryf("upgraded %s to %d", upgrade, ret)
+
+	return ret, nil
+}
+
+// RoverTurn will turn the rover
+func (w *World) RoverTurn(rover string, bearing roveapi.Bearing) (roveapi.Bearing, error) {
+	w.worldMutex.Lock()
+	defer w.worldMutex.Unlock()
+
+	r, ok := w.Rovers[rover]
+	if !ok {
+		return roveapi.Bearing_BearingUnknown, fmt.Errorf("no rover matching id")
+	}
+
+	// Set the new bearing
+	r.Bearing = bearing
+	// Reset the movement ticks
+	r.MoveTicks = 0
+
+	return r.Bearing, nil
+}
+
+// RoverRepair will turn the rover
+func (w *World) RoverRepair(rover string) (int, error) {
+	w.worldMutex.Lock()
+	defer w.worldMutex.Unlock()
+
+	r, ok := w.Rovers[rover]
+	if !ok {
+		return 0, fmt.Errorf("no rover matching id")
+	}
+
+	// Can't repair past max
+	if r.Integrity >= r.MaximumIntegrity {
+		return r.Integrity, nil
+	}
+
+	// Find rover parts in inventory
+	for i, o := range r.Inventory {
+		if o.Type == roveapi.Object_RoverParts {
+
+			// Copy-erase from slice
+			r.Inventory[i] = r.Inventory[len(r.Inventory)-1]
+			r.Inventory = r.Inventory[:len(r.Inventory)-1]
+
+			// Repair
+			r.Integrity = r.Integrity + 1
+			r.AddLogEntryf("repaired self to %d", r.Integrity)
+			break
+		}
+	}
+
+	return r.Integrity, nil
+}
+
 // RadarFromRover can be used to query what a rover can currently see
 func (w *World) RadarFromRover(rover string) (radar []roveapi.Tile, objs []roveapi.Object, err error) {
 	w.worldMutex.RLock()
@@ -409,10 +618,7 @@ func (w *World) RadarFromRover(rover string) (radar []roveapi.Tile, objs []rovea
 }
 
 // RoverCommands returns current commands for the given rover
-func (w *World) RoverCommands(rover string) (incoming []Command, queued []Command) {
-	if c, ok := w.CommandIncoming[rover]; ok {
-		incoming = c
-	}
+func (w *World) RoverCommands(rover string) (queued CommandStream) {
 	if c, ok := w.CommandQueue[rover]; ok {
 		queued = c
 	}
@@ -420,27 +626,34 @@ func (w *World) RoverCommands(rover string) (incoming []Command, queued []Comman
 }
 
 // Enqueue will queue the commands given
-func (w *World) Enqueue(rover string, commands ...Command) error {
+func (w *World) Enqueue(rover string, commands ...*roveapi.Command) error {
 
 	// First validate the commands
 	for _, c := range commands {
 		switch c.Command {
-		case roveapi.CommandType_move:
-			if c.Bearing == roveapi.Bearing_BearingUnknown {
-				return fmt.Errorf("bearing must be valid")
-			}
 		case roveapi.CommandType_broadcast:
-			if len(c.Message) > 3 {
-				return fmt.Errorf("too many characters in message (limit 3): %d", len(c.Message))
+			if len(c.GetData()) > 3 {
+				return fmt.Errorf("too many characters in message (limit 3): %d", len(c.GetData()))
 			}
-			for _, b := range c.Message {
+			for _, b := range c.GetData() {
 				if b < 37 || b > 126 {
 					return fmt.Errorf("invalid message character: %c", b)
 				}
 			}
+		case roveapi.CommandType_turn:
+			if c.GetBearing() == roveapi.Bearing_BearingUnknown {
+				return fmt.Errorf("turn command given unknown bearing")
+			}
+		case roveapi.CommandType_upgrade:
+			if c.GetUpgrade() == roveapi.RoverUpgrade_RoverUpgradeUnknown {
+				return fmt.Errorf("upgrade command given unknown upgrade")
+			}
+		case roveapi.CommandType_wait:
+		case roveapi.CommandType_toggle:
 		case roveapi.CommandType_stash:
 		case roveapi.CommandType_repair:
-		case roveapi.CommandType_recharge:
+		case roveapi.CommandType_salvage:
+		case roveapi.CommandType_transfer:
 			// Nothing to verify
 		default:
 			return fmt.Errorf("unknown command: %s", c.Command)
@@ -451,39 +664,31 @@ func (w *World) Enqueue(rover string, commands ...Command) error {
 	w.cmdMutex.Lock()
 	defer w.cmdMutex.Unlock()
 
-	// Override the incoming command set
-	w.CommandIncoming[rover] = commands
+	w.CommandQueue[rover] = commands
 
 	return nil
 }
 
-// EnqueueAllIncoming will enqueue the incoming commands
-func (w *World) EnqueueAllIncoming() {
-	// Add any incoming commands from this tick and clear that queue
-	for id, incoming := range w.CommandIncoming {
-		commands := w.CommandQueue[id]
-		commands = append(commands, incoming...)
-		w.CommandQueue[id] = commands
-	}
-	w.CommandIncoming = make(map[string]CommandStream)
-}
-
-// ExecuteCommandQueues will execute any commands in the current command queue
-func (w *World) ExecuteCommandQueues() {
+// Tick will execute any commands in the current command queue and tick the world
+func (w *World) Tick() {
 	w.cmdMutex.Lock()
 	defer w.cmdMutex.Unlock()
 
 	// Iterate through all the current commands
 	for rover, cmds := range w.CommandQueue {
 		if len(cmds) != 0 {
-			// Extract the first command in the queue
-			c := cmds[0]
-			w.CommandQueue[rover] = cmds[1:]
 
 			// Execute the command
-			if err := w.ExecuteCommand(&c, rover); err != nil {
+			if done, err := w.ExecuteCommand(cmds[0], rover); err != nil {
 				log.Println(err)
 				// TODO: Report this error somehow
+
+			} else if done {
+				// Extract the first command in the queue
+				// Only if the command queue still has entries (the command may have modified this queue)
+				if _, ok := w.CommandQueue[rover]; ok {
+					w.CommandQueue[rover] = cmds[1:]
+				}
 			}
 
 		} else {
@@ -492,54 +697,115 @@ func (w *World) ExecuteCommandQueues() {
 		}
 	}
 
-	// Add any incoming commands from this tick and clear that queue
-	w.EnqueueAllIncoming()
+	// Move all the rovers based on current wind and sails
+	for n, r := range w.Rovers {
+		// Skip if we're not catching the wind
+		if r.SailPosition != roveapi.SailPosition_CatchingWind {
+			continue
+		}
+
+		// Increment the current move ticks
+		r.MoveTicks++
+
+		// Get the difference between the two bearings
+		// Normalise, we don't care about clockwise/anticlockwise
+		diff := maths.Abs(int(w.Wind - r.Bearing))
+		if diff > 4 {
+			diff = 8 - diff
+		}
+
+		// Calculate the travel "ticks"
+		var ticksToMove int
+		switch diff {
+		case 0:
+			// Going with the wind, travel at base speed of once every 4 ticks
+			ticksToMove = ticksPerNormalMove
+		case 1:
+			// At a slight angle, we can go a little faster
+			ticksToMove = ticksPerNormalMove / 2
+		case 2:
+			// Perpendicular to wind, max speed
+			ticksToMove = 1
+		case 3:
+			// Heading at 45 degrees into the wind, back to min speed
+			ticksToMove = ticksPerNormalMove
+		case 4:
+			// Heading durectly into the wind, no movement at all
+		default:
+			log.Fatalf("bearing difference of %d should be impossible", diff)
+		}
+
+		// If we've incremented over the current move ticks on the rover, we can try and make the move
+		if ticksToMove != 0 && r.MoveTicks >= ticksToMove {
+			_, err := w.TryMoveRover(n, r.Bearing)
+			if err != nil {
+				log.Println(err)
+				// TODO: Report this error somehow
+			}
+
+			// Reset the move ticks
+			r.MoveTicks = 0
+		}
+	}
+
+	// Check all rover integrities
+	for _, r := range w.Rovers {
+		if r.Integrity <= 0 {
+			// The rover has died destroy it
+			err := w.DestroyRover(r.Name)
+			if err != nil {
+				log.Println(err)
+				// TODO: Report this error somehow
+			}
+
+			// Spawn a new one for this account
+			_, err = w.SpawnRover(r.Owner)
+			if err != nil {
+				log.Println(err)
+				// TODO: Report this error somehow
+			}
+		}
+	}
 
 	// Increment the current tick count
 	w.CurrentTicks++
+
+	// Change the wind every day
+	if (w.CurrentTicks % w.TicksPerDay) == 0 {
+		w.Wind = roveapi.Bearing((rand.Int() % 8) + 1) // Random cardinal bearing
+	}
 }
 
 // ExecuteCommand will execute a single command
-func (w *World) ExecuteCommand(c *Command, rover string) (err error) {
-	log.Printf("Executing command: %+v for %s\n", *c, rover)
+func (w *World) ExecuteCommand(c *roveapi.Command, rover string) (done bool, err error) {
+	log.Printf("Executing command: %+v for %s\n", c.Command, rover)
 
 	switch c.Command {
-	case roveapi.CommandType_move:
-		if _, err := w.MoveRover(rover, c.Bearing); err != nil {
-			return err
-		}
-
+	case roveapi.CommandType_toggle:
+		_, err = w.RoverToggle(rover)
 	case roveapi.CommandType_stash:
-		if _, err := w.RoverStash(rover); err != nil {
-			return err
-		}
-
+		_, err = w.RoverStash(rover)
 	case roveapi.CommandType_repair:
-		r, err := w.GetRover(rover)
-		if err != nil {
-			return err
-		}
-		// Consume an inventory item to repair if possible
-		if len(r.Inventory) > 0 && r.Integrity < r.MaximumIntegrity {
-			r.Inventory = r.Inventory[:len(r.Inventory)-1]
-			r.Integrity = r.Integrity + 1
-			r.AddLogEntryf("repaired self to %d", r.Integrity)
-			w.Rovers[rover] = r
-		}
-	case roveapi.CommandType_recharge:
-		_, err := w.RoverRecharge(rover)
-		if err != nil {
-			return err
-		}
+		_, err = w.RoverRepair(rover)
 	case roveapi.CommandType_broadcast:
-		if err := w.RoverBroadcast(rover, c.Message); err != nil {
-			return err
-		}
+		err = w.RoverBroadcast(rover, c.GetData())
+	case roveapi.CommandType_turn:
+		_, err = w.RoverTurn(rover, c.GetBearing())
+	case roveapi.CommandType_salvage:
+		_, err = w.RoverSalvage(rover)
+	case roveapi.CommandType_transfer:
+		_, err = w.RoverTransfer(rover)
+	case roveapi.CommandType_upgrade:
+		_, err = w.RoverUpgrade(rover, c.GetUpgrade())
+	case roveapi.CommandType_wait:
+		// Nothing to do
 	default:
-		return fmt.Errorf("unknown command: %s", c.Command)
+		return true, fmt.Errorf("unknown command: %s", c.Command)
 	}
 
-	return
+	// Decrement the repeat number
+	c.Repeat--
+	return c.Repeat < 0, err
 }
 
 // Daytime returns if it's currently daytime
diff --git a/pkg/rove/world_test.go b/pkg/rove/world_test.go
index 7cf2483..f085d89 100644
--- a/pkg/rove/world_test.go
+++ b/pkg/rove/world_test.go
@@ -3,7 +3,6 @@ package rove
 import (
 	"testing"
 
-	"github.com/mdiluz/rove/pkg/atlas"
 	"github.com/mdiluz/rove/pkg/maths"
 	"github.com/mdiluz/rove/proto/roveapi"
 	"github.com/stretchr/testify/assert"
@@ -19,9 +18,9 @@ func TestNewWorld(t *testing.T) {
 
 func TestWorld_CreateRover(t *testing.T) {
 	world := NewWorld(8)
-	a, err := world.SpawnRover()
+	a, err := world.SpawnRover("")
 	assert.NoError(t, err)
-	b, err := world.SpawnRover()
+	b, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
 	// Basic duplicate check
@@ -34,7 +33,7 @@ func TestWorld_CreateRover(t *testing.T) {
 
 func TestWorld_GetRover(t *testing.T) {
 	world := NewWorld(4)
-	a, err := world.SpawnRover()
+	a, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
 	rover, err := world.GetRover(a)
@@ -45,9 +44,9 @@ func TestWorld_GetRover(t *testing.T) {
 
 func TestWorld_DestroyRover(t *testing.T) {
 	world := NewWorld(1)
-	a, err := world.SpawnRover()
+	a, err := world.SpawnRover("")
 	assert.NoError(t, err)
-	b, err := world.SpawnRover()
+	b, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
 	err = world.DestroyRover(a)
@@ -63,7 +62,7 @@ func TestWorld_DestroyRover(t *testing.T) {
 
 func TestWorld_GetSetMovePosition(t *testing.T) {
 	world := NewWorld(4)
-	a, err := world.SpawnRover()
+	a, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
 	pos := maths.Vector{
@@ -79,49 +78,47 @@ func TestWorld_GetSetMovePosition(t *testing.T) {
 	assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
 
 	b := roveapi.Bearing_North
-	newPos, err = world.MoveRover(a, b)
+	newPos, err = world.TryMoveRover(a, b)
 	assert.NoError(t, err, "Failed to set position for rover")
 	pos.Add(maths.Vector{X: 0, Y: 1})
 	assert.Equal(t, pos, newPos, "Failed to correctly move position for rover")
 
 	rover, err := world.GetRover(a)
 	assert.NoError(t, err, "Failed to get rover information")
-	assert.Equal(t, rover.MaximumCharge-1, rover.Charge, "Rover should have lost charge for moving")
 	assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "moved", "Rover logs should contain the move")
 
 	// Place a tile in front of the rover
-	world.Atlas.SetObject(maths.Vector{X: 0, Y: 2}, atlas.Object{Type: roveapi.Object_RockLarge})
-	newPos, err = world.MoveRover(a, b)
+	world.Atlas.SetObject(maths.Vector{X: 0, Y: 2}, Object{Type: roveapi.Object_RockLarge})
+	newPos, err = world.TryMoveRover(a, b)
 	assert.NoError(t, err, "Failed to move rover")
 	assert.Equal(t, pos, newPos, "Failed to correctly not move position for rover into wall")
-
-	rover, err = world.GetRover(a)
-	assert.NoError(t, err, "Failed to get rover information")
-	assert.Equal(t, rover.MaximumCharge-2, rover.Charge, "Rover should have lost charge for move attempt")
 }
 
 func TestWorld_RadarFromRover(t *testing.T) {
 	// Create world that should have visible walls on the radar
 	world := NewWorld(2)
-	a, err := world.SpawnRover()
+	a, err := world.SpawnRover("")
 	assert.NoError(t, err)
-	b, err := world.SpawnRover()
+	b, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
 	// Warp the rovers into position
 	bpos := maths.Vector{X: -3, Y: -3}
+	world.Atlas.SetObject(bpos, Object{Type: roveapi.Object_ObjectUnknown})
 	assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover")
+	world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
 	assert.NoError(t, world.WarpRover(a, maths.Vector{X: 0, Y: 0}), "Failed to warp rover")
 
+	r, err := world.GetRover(a)
+	assert.NoError(t, err)
+
 	radar, objs, err := world.RadarFromRover(a)
 	assert.NoError(t, err, "Failed to get radar from rover")
-	fullRange := 4 + 4 + 1
+	fullRange := r.Range + r.Range + 1
 	assert.Equal(t, fullRange*fullRange, len(radar), "Radar returned wrong length")
 	assert.Equal(t, fullRange*fullRange, len(objs), "Radar returned wrong length")
 
-	// Test the expected values
-	assert.Equal(t, roveapi.Object_RoverLive, objs[1+fullRange])
-	assert.Equal(t, roveapi.Object_RoverLive, objs[4+4*fullRange])
+	// TODO: Verify the other rover is on the radar
 
 	// Check the radar results are stable
 	radar1, objs1, err := world.RadarFromRover(a)
@@ -132,82 +129,11 @@ func TestWorld_RadarFromRover(t *testing.T) {
 	assert.Equal(t, objs1, objs2)
 }
 
-func TestWorld_RoverStash(t *testing.T) {
-	world := NewWorld(2)
-	a, err := world.SpawnRover()
-	assert.NoError(t, err)
-
-	pos := maths.Vector{
-		X: 0.0,
-		Y: 0.0,
-	}
-
-	world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_ObjectUnknown})
-	err = world.WarpRover(a, pos)
-	assert.NoError(t, err, "Failed to set position for rover")
-
-	rover, err := world.GetRover(a)
-	assert.NoError(t, err, "Failed to get rover")
-
-	for i := 0; i < rover.Capacity; i++ {
-		// Place an object
-		world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
-
-		// Pick it up
-		o, err := world.RoverStash(a)
-		assert.NoError(t, err, "Failed to stash")
-		assert.Equal(t, roveapi.Object_RockSmall, o, "Failed to get correct object")
-
-		// Check it's gone
-		_, obj := world.Atlas.QueryPosition(pos)
-		assert.Equal(t, roveapi.Object_ObjectUnknown, obj.Type, "Stash failed to remove object from atlas")
-
-		// Check we have it
-		inv, err := world.RoverInventory(a)
-		assert.NoError(t, err, "Failed to get inventory")
-		assert.Equal(t, i+1, len(inv))
-		assert.Equal(t, atlas.Object{Type: roveapi.Object_RockSmall}, inv[i])
-
-		// Check that this did reduce the charge
-		info, err := world.GetRover(a)
-		assert.NoError(t, err, "Failed to get rover")
-		assert.Equal(t, info.MaximumCharge-(i+1), info.Charge, "Rover lost charge for stash")
-		assert.Contains(t, info.Logs[len(info.Logs)-1].Text, "stashed", "Rover logs should contain the move")
-	}
-
-	// Recharge the rover
-	for i := 0; i < rover.MaximumCharge; i++ {
-		_, err = world.RoverRecharge(a)
-		assert.NoError(t, err)
-
-	}
-
-	// Place an object
-	world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
-
-	// Try to pick it up
-	o, err := world.RoverStash(a)
-	assert.NoError(t, err, "Failed to stash")
-	assert.Equal(t, roveapi.Object_ObjectUnknown, o, "Failed to get correct object")
-
-	// Check it's still there
-	_, obj := world.Atlas.QueryPosition(pos)
-	assert.Equal(t, roveapi.Object_RockSmall, obj.Type, "Stash failed to remove object from atlas")
-
-	// Check we don't have it
-	inv, err := world.RoverInventory(a)
-	assert.NoError(t, err, "Failed to get inventory")
-	assert.Equal(t, rover.Capacity, len(inv))
-
-	// Check that this didn't reduce the charge
-	info, err := world.GetRover(a)
-	assert.NoError(t, err, "Failed to get rover")
-	assert.Equal(t, info.MaximumCharge, info.Charge, "Rover lost charge for non-stash")
-}
-
 func TestWorld_RoverDamage(t *testing.T) {
 	world := NewWorld(2)
-	a, err := world.SpawnRover()
+	acc, err := world.Accountant.RegisterAccount("tmp")
+	assert.NoError(t, err)
+	a, err := world.SpawnRover(acc.Name)
 	assert.NoError(t, err)
 
 	pos := maths.Vector{
@@ -215,15 +141,16 @@ func TestWorld_RoverDamage(t *testing.T) {
 		Y: 0.0,
 	}
 
+	world.Atlas.SetObject(pos, Object{Type: roveapi.Object_ObjectUnknown})
 	err = world.WarpRover(a, pos)
 	assert.NoError(t, err, "Failed to set position for rover")
 
 	info, err := world.GetRover(a)
 	assert.NoError(t, err, "couldn't get rover info")
 
-	world.Atlas.SetObject(maths.Vector{X: 0.0, Y: 1.0}, atlas.Object{Type: roveapi.Object_RockLarge})
+	world.Atlas.SetObject(maths.Vector{X: 0.0, Y: 1.0}, Object{Type: roveapi.Object_RockLarge})
 
-	vec, err := world.MoveRover(a, roveapi.Bearing_North)
+	vec, err := world.TryMoveRover(a, roveapi.Bearing_North)
 	assert.NoError(t, err, "Failed to move rover")
 	assert.Equal(t, pos, vec, "Rover managed to move into large rock")
 
@@ -231,102 +158,29 @@ func TestWorld_RoverDamage(t *testing.T) {
 	assert.NoError(t, err, "couldn't get rover info")
 	assert.Equal(t, info.Integrity-1, newinfo.Integrity, "rover should have lost integrity")
 	assert.Contains(t, newinfo.Logs[len(newinfo.Logs)-1].Text, "collision", "Rover logs should contain the collision")
-}
 
-func TestWorld_RoverRepair(t *testing.T) {
-	world := NewWorld(2)
-	a, err := world.SpawnRover()
-	assert.NoError(t, err)
-
-	pos := maths.Vector{
-		X: 0.0,
-		Y: 0.0,
+	// Keep moving to damage the rover
+	for i := 0; i < info.Integrity-1; i++ {
+		vec, err := world.TryMoveRover(a, roveapi.Bearing_North)
+		assert.NoError(t, err, "Failed to move rover")
+		assert.Equal(t, pos, vec, "Rover managed to move into large rock")
 	}
 
-	world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_ObjectUnknown})
+	// Tick the world to check for rover deaths
+	world.Tick()
 
-	err = world.WarpRover(a, pos)
-	assert.NoError(t, err, "Failed to set position for rover")
-
-	originalInfo, err := world.GetRover(a)
-	assert.NoError(t, err, "couldn't get rover info")
-
-	// Pick up something to repair with
-	world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
-	o, err := world.RoverStash(a)
-	assert.NoError(t, err, "Failed to stash")
-	assert.Equal(t, roveapi.Object_RockSmall, o, "Failed to get correct object")
-
-	world.Atlas.SetObject(maths.Vector{X: 0.0, Y: 1.0}, atlas.Object{Type: roveapi.Object_RockLarge})
-
-	// Try and bump into the rock
-	vec, err := world.MoveRover(a, roveapi.Bearing_North)
-	assert.NoError(t, err, "Failed to move rover")
-	assert.Equal(t, pos, vec, "Rover managed to move into large rock")
-
-	newinfo, err := world.GetRover(a)
-	assert.NoError(t, err, "couldn't get rover info")
-	assert.Equal(t, originalInfo.Integrity-1, newinfo.Integrity, "rover should have lost integrity")
-
-	err = world.ExecuteCommand(&Command{Command: roveapi.CommandType_repair}, a)
-	assert.NoError(t, err, "Failed to repair rover")
-
-	newinfo, err = world.GetRover(a)
-	assert.NoError(t, err, "couldn't get rover info")
-	assert.Equal(t, originalInfo.Integrity, newinfo.Integrity, "rover should have gained integrity")
-	assert.Contains(t, newinfo.Logs[len(newinfo.Logs)-1].Text, "repair", "Rover logs should contain the repair")
-
-	// Check again that it can't repair past the max
-	world.Atlas.SetObject(pos, atlas.Object{Type: roveapi.Object_RockSmall})
-	o, err = world.RoverStash(a)
-	assert.NoError(t, err, "Failed to stash")
-	assert.Equal(t, roveapi.Object_RockSmall, o, "Failed to get correct object")
-
-	err = world.ExecuteCommand(&Command{Command: roveapi.CommandType_repair}, a)
-	assert.NoError(t, err, "Failed to repair rover")
-
-	newinfo, err = world.GetRover(a)
-	assert.NoError(t, err, "couldn't get rover info")
-	assert.Equal(t, originalInfo.Integrity, newinfo.Integrity, "rover should have kept the same integrity")
-}
-
-func TestWorld_Charge(t *testing.T) {
-	world := NewWorld(4)
-	a, err := world.SpawnRover()
-	assert.NoError(t, err)
-
-	// Get the rover information
-	rover, err := world.GetRover(a)
-	assert.NoError(t, err, "Failed to get rover information")
-	assert.Equal(t, rover.MaximumCharge, rover.Charge, "Rover should start with maximum charge")
-
-	// Use up all the charge
-	for i := 0; i < rover.MaximumCharge; i++ {
-		// Get the initial position
-		initialPos, err := world.RoverPosition(a)
-		assert.NoError(t, err, "Failed to get position for rover")
-
-		// Ensure the path ahead is empty
-		world.Atlas.SetTile(initialPos.Added(maths.BearingToVector(roveapi.Bearing_North)), roveapi.Tile_Rock)
-		world.Atlas.SetObject(initialPos.Added(maths.BearingToVector(roveapi.Bearing_North)), atlas.Object{Type: roveapi.Object_ObjectUnknown})
-
-		// Try and move north (along unblocked path)
-		newPos, err := world.MoveRover(a, roveapi.Bearing_North)
-		assert.NoError(t, err, "Failed to set position for rover")
-		assert.Equal(t, initialPos.Added(maths.BearingToVector(roveapi.Bearing_North)), newPos, "Failed to correctly move position for rover")
-
-		// Ensure rover lost charge
-		rover, err := world.GetRover(a)
-		assert.NoError(t, err, "Failed to get rover information")
-		assert.Equal(t, rover.MaximumCharge-(i+1), rover.Charge, "Rover should have lost charge")
-	}
+	// Rover should have been destroyed now
+	_, err = world.GetRover(a)
+	assert.Error(t, err)
 
+	_, obj := world.Atlas.QueryPosition(info.Pos)
+	assert.Equal(t, roveapi.Object_RoverDormant, obj.Type)
 }
 
 func TestWorld_Daytime(t *testing.T) {
 	world := NewWorld(1)
 
-	a, err := world.SpawnRover()
+	a, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
 	// Remove rover charge
@@ -342,7 +196,7 @@ func TestWorld_Daytime(t *testing.T) {
 	// Loop for half the day
 	for i := 0; i < world.TicksPerDay/2; i++ {
 		assert.True(t, world.Daytime())
-		world.ExecuteCommandQueues()
+		world.Tick()
 	}
 
 	// Remove rover charge again
@@ -358,20 +212,22 @@ func TestWorld_Daytime(t *testing.T) {
 	// Loop for half the day
 	for i := 0; i < world.TicksPerDay/2; i++ {
 		assert.False(t, world.Daytime())
-		world.ExecuteCommandQueues()
+		world.Tick()
 	}
 }
 
 func TestWorld_Broadcast(t *testing.T) {
 	world := NewWorld(8)
 
-	a, err := world.SpawnRover()
+	a, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
-	b, err := world.SpawnRover()
+	b, err := world.SpawnRover("")
 	assert.NoError(t, err)
 
 	// Warp rovers near to eachother
+	world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
+	world.Atlas.SetObject(maths.Vector{X: 1, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
 	assert.NoError(t, world.WarpRover(a, maths.Vector{X: 0, Y: 0}))
 	assert.NoError(t, world.WarpRover(b, maths.Vector{X: 1, Y: 0}))
 
@@ -390,7 +246,7 @@ func TestWorld_Broadcast(t *testing.T) {
 	assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "ABC", "Rover A should have logged it's broadcast")
 
 	// Warp B outside of the range of A
-	world.Atlas.SetObject(maths.Vector{X: ra.Range, Y: 0}, atlas.Object{Type: roveapi.Object_ObjectUnknown})
+	world.Atlas.SetObject(maths.Vector{X: ra.Range, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
 	assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range, Y: 0}))
 
 	// Broadcast from a again
@@ -407,7 +263,7 @@ func TestWorld_Broadcast(t *testing.T) {
 	assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "XYZ", "Rover A should have logged it's broadcast")
 
 	// Warp B outside of the range of A
-	world.Atlas.SetObject(maths.Vector{X: ra.Range + 1, Y: 0}, atlas.Object{Type: roveapi.Object_ObjectUnknown})
+	world.Atlas.SetObject(maths.Vector{X: ra.Range + 1, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
 	assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range + 1, Y: 0}))
 
 	// Broadcast from a again
@@ -423,3 +279,81 @@ func TestWorld_Broadcast(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "HJK", "Rover A should have logged it's broadcast")
 }
+
+func TestWorld_Sailing(t *testing.T) {
+	world := NewWorld(8)
+	world.Tick()                       // One initial tick to set the wind direction the first time
+	world.Wind = roveapi.Bearing_North // Set the wind direction to north
+
+	name, err := world.SpawnRover("")
+	assert.NoError(t, err)
+
+	// Warp the rover to 0,0 after clearing it
+	world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
+	assert.NoError(t, world.WarpRover(name, maths.Vector{X: 0, Y: 0}))
+
+	s, err := world.RoverToggle(name)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.SailPosition_CatchingWind, s)
+
+	b, err := world.RoverTurn(name, roveapi.Bearing_North)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.Bearing_North, b)
+
+	// Clear the space to the north
+	world.Atlas.SetObject(maths.Vector{X: 0, Y: 1}, Object{Type: roveapi.Object_ObjectUnknown})
+
+	// Tick the world and check we've moved not moved
+	world.Tick()
+	info, err := world.GetRover(name)
+	assert.NoError(t, err)
+	assert.Equal(t, maths.Vector{Y: 0}, info.Pos)
+
+	// Loop a few more times
+	for i := 0; i < ticksPerNormalMove-2; i++ {
+		world.Tick()
+		info, err := world.GetRover(name)
+		assert.NoError(t, err)
+		assert.Equal(t, maths.Vector{Y: 0}, info.Pos)
+	}
+
+	// Now check we've moved (after the TicksPerNormalMove number of ticks)
+	world.Tick()
+	info, err = world.GetRover(name)
+	assert.NoError(t, err)
+	assert.Equal(t, maths.Vector{Y: 1}, info.Pos)
+
+	// Reset the world ticks back to stop any wind changes etc.
+	world.CurrentTicks = 1
+
+	// Face the rover south, into the wind
+	b, err = world.RoverTurn(name, roveapi.Bearing_South)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.Bearing_South, b)
+
+	// Tick a bunch, we should never move
+	for i := 0; i < ticksPerNormalMove*2; i++ {
+		world.Tick()
+		info, err := world.GetRover(name)
+		assert.NoError(t, err)
+		assert.Equal(t, maths.Vector{Y: 1}, info.Pos)
+	}
+
+	// Reset the world ticks back to stop any wind changes etc.
+	world.CurrentTicks = 1
+	world.Wind = roveapi.Bearing_SouthEast // Set up a south easternly wind
+
+	// Turn the rover perpendicular
+	b, err = world.RoverTurn(name, roveapi.Bearing_NorthEast)
+	assert.NoError(t, err)
+	assert.Equal(t, roveapi.Bearing_NorthEast, b)
+
+	// Clear a space
+	world.Atlas.SetObject(maths.Vector{X: 1, Y: 2}, Object{Type: roveapi.Object_ObjectUnknown})
+
+	// Now check we've moved immediately
+	world.Tick()
+	info, err = world.GetRover(name)
+	assert.NoError(t, err)
+	assert.Equal(t, maths.Vector{X: 1, Y: 2}, info.Pos)
+}
diff --git a/pkg/rove/worldgen.go b/pkg/rove/worldgen.go
new file mode 100644
index 0000000..6cb425f
--- /dev/null
+++ b/pkg/rove/worldgen.go
@@ -0,0 +1,70 @@
+package rove
+
+import (
+	"github.com/mdiluz/rove/pkg/maths"
+	"github.com/mdiluz/rove/proto/roveapi"
+	"github.com/ojrac/opensimplex-go"
+)
+
+// WorldGen describes a world gen algorythm
+type WorldGen interface {
+	// GetTile generates a tile for a location
+	GetTile(v maths.Vector) roveapi.Tile
+
+	// GetObject generates an object for a location
+	GetObject(v maths.Vector) Object
+}
+
+// NoiseWorldGen returns a noise based world generator
+type NoiseWorldGen struct {
+	// noise describes the noise function
+	noise opensimplex.Noise
+}
+
+// NewNoiseWorldGen creates a new noise based world generator
+func NewNoiseWorldGen(seed int64) WorldGen {
+	return &NoiseWorldGen{
+		noise: opensimplex.New(seed),
+	}
+}
+
+const (
+	terrainNoiseScale = 15
+	rockNoiseScale    = 3
+	partsNoiseScale   = 2
+)
+
+// GetTile returns the chosen tile at a location
+func (g *NoiseWorldGen) GetTile(v maths.Vector) roveapi.Tile {
+	t := g.noise.Eval2(float64(v.X)/terrainNoiseScale, float64(v.Y)/terrainNoiseScale)
+	switch {
+	case t > 0.5:
+		return roveapi.Tile_Gravel
+	case t > 0.05:
+		return roveapi.Tile_Sand
+	default:
+		return roveapi.Tile_Rock
+	}
+}
+
+// GetObject returns the chosen object at a location
+func (g *NoiseWorldGen) GetObject(v maths.Vector) (obj Object) {
+	r := g.noise.Eval2(float64(v.X)/rockNoiseScale, float64(v.Y)/rockNoiseScale)
+	switch {
+	// Prioritise rocks
+	case r > 0.6:
+		obj.Type = roveapi.Object_RockLarge
+	case r > 0.5:
+		obj.Type = roveapi.Object_RockSmall
+
+	default:
+		// Otherwise, try some rover parts
+		p := g.noise.Eval2(float64(v.X)/partsNoiseScale, float64(v.Y)/partsNoiseScale)
+		switch {
+		case p > 0.7:
+			obj.Type = roveapi.Object_RoverParts
+		}
+	}
+
+	return obj
+}
diff --git a/proto/roveapi/roveapi.pb.go b/proto/roveapi/roveapi.pb.go
index e35c863..81a6cb5 100644
--- a/proto/roveapi/roveapi.pb.go
+++ b/proto/roveapi/roveapi.pb.go
@@ -39,35 +39,51 @@ type CommandType int32
 
 const (
 	CommandType_none CommandType = 0
-	// Move the rover in a direction, requires bearing
-	CommandType_move CommandType = 1
+	// Waits before performing the next command
+	CommandType_wait CommandType = 1
+	// Toggles the sails, either catching the wind, or charging from the sun
+	CommandType_toggle CommandType = 2
+	// Turns the rover in the specified bearing (requires bearing)
+	CommandType_turn CommandType = 3
 	// Stashes item at current location in rover inventory
-	CommandType_stash CommandType = 2
+	CommandType_stash CommandType = 4
 	// Repairs the rover using an inventory object
-	CommandType_repair CommandType = 3
-	// Waits a tick to add more charge to the rover
-	CommandType_recharge CommandType = 4
-	// Broadcasts a message to nearby rovers
-	CommandType_broadcast CommandType = 5
+	CommandType_repair CommandType = 5
+	// Broadcasts a message to nearby rovers (requires data)
+	CommandType_broadcast CommandType = 6
+	// Salvages a neighboring dormant rover for parts
+	CommandType_salvage CommandType = 7
+	// Transfers remote control into dormant rover
+	CommandType_transfer CommandType = 8
+	// Upgrades a chosen rover specification using 5 rover parts
+	CommandType_upgrade CommandType = 9
 )
 
 // Enum value maps for CommandType.
 var (
 	CommandType_name = map[int32]string{
 		0: "none",
-		1: "move",
-		2: "stash",
-		3: "repair",
-		4: "recharge",
-		5: "broadcast",
+		1: "wait",
+		2: "toggle",
+		3: "turn",
+		4: "stash",
+		5: "repair",
+		6: "broadcast",
+		7: "salvage",
+		8: "transfer",
+		9: "upgrade",
 	}
 	CommandType_value = map[string]int32{
 		"none":      0,
-		"move":      1,
-		"stash":     2,
-		"repair":    3,
-		"recharge":  4,
-		"broadcast": 5,
+		"wait":      1,
+		"toggle":    2,
+		"turn":      3,
+		"stash":     4,
+		"repair":    5,
+		"broadcast": 6,
+		"salvage":   7,
+		"transfer":  8,
+		"upgrade":   9,
 	}
 )
 
@@ -98,15 +114,20 @@ func (CommandType) EnumDescriptor() ([]byte, []int) {
 	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{0}
 }
 
+// Bearing represents a compass direction
 type Bearing int32
 
 const (
 	// BearingUnknown an unknown invalid bearing
 	Bearing_BearingUnknown Bearing = 0
 	Bearing_North          Bearing = 1
-	Bearing_East           Bearing = 2
-	Bearing_South          Bearing = 3
-	Bearing_West           Bearing = 4
+	Bearing_NorthEast      Bearing = 2
+	Bearing_East           Bearing = 3
+	Bearing_SouthEast      Bearing = 4
+	Bearing_South          Bearing = 5
+	Bearing_SouthWest      Bearing = 6
+	Bearing_West           Bearing = 7
+	Bearing_NorthWest      Bearing = 8
 )
 
 // Enum value maps for Bearing.
@@ -114,16 +135,24 @@ var (
 	Bearing_name = map[int32]string{
 		0: "BearingUnknown",
 		1: "North",
-		2: "East",
-		3: "South",
-		4: "West",
+		2: "NorthEast",
+		3: "East",
+		4: "SouthEast",
+		5: "South",
+		6: "SouthWest",
+		7: "West",
+		8: "NorthWest",
 	}
 	Bearing_value = map[string]int32{
 		"BearingUnknown": 0,
 		"North":          1,
-		"East":           2,
-		"South":          3,
-		"West":           4,
+		"NorthEast":      2,
+		"East":           3,
+		"SouthEast":      4,
+		"South":          5,
+		"SouthWest":      6,
+		"West":           7,
+		"NorthWest":      8,
 	}
 )
 
@@ -154,6 +183,62 @@ func (Bearing) EnumDescriptor() ([]byte, []int) {
 	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{1}
 }
 
+// Describes the type of upgrade
+type RoverUpgrade int32
+
+const (
+	RoverUpgrade_RoverUpgradeUnknown RoverUpgrade = 0
+	RoverUpgrade_Range               RoverUpgrade = 1
+	RoverUpgrade_Capacity            RoverUpgrade = 2
+	RoverUpgrade_MaximumIntegrity    RoverUpgrade = 3
+	RoverUpgrade_MaximumCharge       RoverUpgrade = 4
+)
+
+// Enum value maps for RoverUpgrade.
+var (
+	RoverUpgrade_name = map[int32]string{
+		0: "RoverUpgradeUnknown",
+		1: "Range",
+		2: "Capacity",
+		3: "MaximumIntegrity",
+		4: "MaximumCharge",
+	}
+	RoverUpgrade_value = map[string]int32{
+		"RoverUpgradeUnknown": 0,
+		"Range":               1,
+		"Capacity":            2,
+		"MaximumIntegrity":    3,
+		"MaximumCharge":       4,
+	}
+)
+
+func (x RoverUpgrade) Enum() *RoverUpgrade {
+	p := new(RoverUpgrade)
+	*p = x
+	return p
+}
+
+func (x RoverUpgrade) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (RoverUpgrade) Descriptor() protoreflect.EnumDescriptor {
+	return file_roveapi_roveapi_proto_enumTypes[2].Descriptor()
+}
+
+func (RoverUpgrade) Type() protoreflect.EnumType {
+	return &file_roveapi_roveapi_proto_enumTypes[2]
+}
+
+func (x RoverUpgrade) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use RoverUpgrade.Descriptor instead.
+func (RoverUpgrade) EnumDescriptor() ([]byte, []int) {
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{2}
+}
+
 // Types of objects
 type Object int32
 
@@ -162,10 +247,15 @@ const (
 	Object_ObjectUnknown Object = 0
 	// RoverLive represents a live rover
 	Object_RoverLive Object = 1
+	// RoverDormant describes a dormant rover
+	Object_RoverDormant Object = 2
 	// RockSmall is a small stashable rock
-	Object_RockSmall Object = 2
+	Object_RockSmall Object = 3
 	// RockLarge is a large blocking rock
-	Object_RockLarge Object = 3
+	Object_RockLarge Object = 4
+	// RoverParts is one unit of rover parts, used for repairing and fixing the
+	// rover
+	Object_RoverParts Object = 5
 )
 
 // Enum value maps for Object.
@@ -173,14 +263,18 @@ var (
 	Object_name = map[int32]string{
 		0: "ObjectUnknown",
 		1: "RoverLive",
-		2: "RockSmall",
-		3: "RockLarge",
+		2: "RoverDormant",
+		3: "RockSmall",
+		4: "RockLarge",
+		5: "RoverParts",
 	}
 	Object_value = map[string]int32{
 		"ObjectUnknown": 0,
 		"RoverLive":     1,
-		"RockSmall":     2,
-		"RockLarge":     3,
+		"RoverDormant":  2,
+		"RockSmall":     3,
+		"RockLarge":     4,
+		"RoverParts":    5,
 	}
 )
 
@@ -195,11 +289,11 @@ func (x Object) String() string {
 }
 
 func (Object) Descriptor() protoreflect.EnumDescriptor {
-	return file_roveapi_roveapi_proto_enumTypes[2].Descriptor()
+	return file_roveapi_roveapi_proto_enumTypes[3].Descriptor()
 }
 
 func (Object) Type() protoreflect.EnumType {
-	return &file_roveapi_roveapi_proto_enumTypes[2]
+	return &file_roveapi_roveapi_proto_enumTypes[3]
 }
 
 func (x Object) Number() protoreflect.EnumNumber {
@@ -208,7 +302,7 @@ func (x Object) Number() protoreflect.EnumNumber {
 
 // Deprecated: Use Object.Descriptor instead.
 func (Object) EnumDescriptor() ([]byte, []int) {
-	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{2}
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{3}
 }
 
 type Tile int32
@@ -251,11 +345,11 @@ func (x Tile) String() string {
 }
 
 func (Tile) Descriptor() protoreflect.EnumDescriptor {
-	return file_roveapi_roveapi_proto_enumTypes[3].Descriptor()
+	return file_roveapi_roveapi_proto_enumTypes[4].Descriptor()
 }
 
 func (Tile) Type() protoreflect.EnumType {
-	return &file_roveapi_roveapi_proto_enumTypes[3]
+	return &file_roveapi_roveapi_proto_enumTypes[4]
 }
 
 func (x Tile) Number() protoreflect.EnumNumber {
@@ -264,7 +358,59 @@ func (x Tile) Number() protoreflect.EnumNumber {
 
 // Deprecated: Use Tile.Descriptor instead.
 func (Tile) EnumDescriptor() ([]byte, []int) {
-	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{3}
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{4}
+}
+
+// SailPosition represents the position of the sola sail
+type SailPosition int32
+
+const (
+	SailPosition_UnknownSailPosition SailPosition = 0
+	// CatchingWind means the sail is catching the wind and moving the rover
+	SailPosition_CatchingWind SailPosition = 1
+	// SolarCharging means the sail is facing the sun and charging
+	SailPosition_SolarCharging SailPosition = 2
+)
+
+// Enum value maps for SailPosition.
+var (
+	SailPosition_name = map[int32]string{
+		0: "UnknownSailPosition",
+		1: "CatchingWind",
+		2: "SolarCharging",
+	}
+	SailPosition_value = map[string]int32{
+		"UnknownSailPosition": 0,
+		"CatchingWind":        1,
+		"SolarCharging":       2,
+	}
+)
+
+func (x SailPosition) Enum() *SailPosition {
+	p := new(SailPosition)
+	*p = x
+	return p
+}
+
+func (x SailPosition) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (SailPosition) Descriptor() protoreflect.EnumDescriptor {
+	return file_roveapi_roveapi_proto_enumTypes[5].Descriptor()
+}
+
+func (SailPosition) Type() protoreflect.EnumType {
+	return &file_roveapi_roveapi_proto_enumTypes[5]
+}
+
+func (x SailPosition) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use SailPosition.Descriptor instead.
+func (SailPosition) EnumDescriptor() ([]byte, []int) {
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{5}
 }
 
 // ServerStatusRequest is an empty placeholder
@@ -555,10 +701,15 @@ type Command struct {
 
 	// The command type
 	Command CommandType `protobuf:"varint,1,opt,name=command,proto3,enum=roveapi.CommandType" json:"command,omitempty"`
-	// Types that are assignable to Data:
-	//	*Command_Bearing
-	//	*Command_Message
-	Data isCommand_Data `protobuf_oneof:"data"`
+	// The number of times to repeat the command after the first
+	Repeat int32 `protobuf:"varint,2,opt,name=repeat,proto3" json:"repeat,omitempty"`
+	// broadcast - a simple message, must be composed of up to 3 printable ASCII
+	// glyphs (32-126)
+	Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
+	// move - the bearing for the rover to turn to
+	Bearing Bearing `protobuf:"varint,4,opt,name=bearing,proto3,enum=roveapi.Bearing" json:"bearing,omitempty"`
+	// upgrade - the upgrade to apply to the rover
+	Upgrade RoverUpgrade `protobuf:"varint,5,opt,name=upgrade,proto3,enum=roveapi.RoverUpgrade" json:"upgrade,omitempty"`
 }
 
 func (x *Command) Reset() {
@@ -600,48 +751,34 @@ func (x *Command) GetCommand() CommandType {
 	return CommandType_none
 }
 
-func (m *Command) GetData() isCommand_Data {
-	if m != nil {
-		return m.Data
+func (x *Command) GetRepeat() int32 {
+	if x != nil {
+		return x.Repeat
+	}
+	return 0
+}
+
+func (x *Command) GetData() []byte {
+	if x != nil {
+		return x.Data
 	}
 	return nil
 }
 
 func (x *Command) GetBearing() Bearing {
-	if x, ok := x.GetData().(*Command_Bearing); ok {
+	if x != nil {
 		return x.Bearing
 	}
 	return Bearing_BearingUnknown
 }
 
-func (x *Command) GetMessage() []byte {
-	if x, ok := x.GetData().(*Command_Message); ok {
-		return x.Message
+func (x *Command) GetUpgrade() RoverUpgrade {
+	if x != nil {
+		return x.Upgrade
 	}
-	return nil
+	return RoverUpgrade_RoverUpgradeUnknown
 }
 
-type isCommand_Data interface {
-	isCommand_Data()
-}
-
-type Command_Bearing struct {
-	// A bearing
-	// Used with MOVE
-	Bearing Bearing `protobuf:"varint,2,opt,name=bearing,proto3,enum=roveapi.Bearing,oneof"`
-}
-
-type Command_Message struct {
-	// A simple message, must be composed of printable ASCII glyphs (32-126)
-	// maximum of three characters
-	// Used with BROADCAST
-	Message []byte `protobuf:"bytes,3,opt,name=message,proto3,oneof"`
-}
-
-func (*Command_Bearing) isCommand_Data() {}
-
-func (*Command_Message) isCommand_Data() {}
-
 // CommandRequest describes a set of commands to be requested for the rover
 type CommandRequest struct {
 	state         protoimpl.MessageState
@@ -1019,42 +1156,267 @@ func (x *Vector) GetY() int32 {
 	return 0
 }
 
-// StatusResponse is the response given to a status request
-type StatusResponse struct {
+type RoverSpecifications struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
 	// The name of the rover
 	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
-	// Position of the rover in world coordinates
-	Position *Vector `protobuf:"bytes,2,opt,name=position,proto3" json:"position,omitempty"`
 	// The range of this rover's radar and broadcasting
-	Range int32 `protobuf:"varint,3,opt,name=range,proto3" json:"range,omitempty"`
-	// The items in the rover inventory
-	Inventory []byte `protobuf:"bytes,4,opt,name=inventory,proto3" json:"inventory,omitempty"`
+	Range int32 `protobuf:"varint,2,opt,name=range,proto3" json:"range,omitempty"`
 	// The capacity of the inventory
-	Capacity int32 `protobuf:"varint,5,opt,name=capacity,proto3" json:"capacity,omitempty"`
-	// The current health of the rover
-	Integrity int32 `protobuf:"varint,6,opt,name=integrity,proto3" json:"integrity,omitempty"`
+	Capacity int32 `protobuf:"varint,3,opt,name=capacity,proto3" json:"capacity,omitempty"`
 	// The maximum health of the rover
-	MaximumIntegrity int32 `protobuf:"varint,7,opt,name=maximumIntegrity,proto3" json:"maximumIntegrity,omitempty"`
-	// The energy stored in the rover
-	Charge int32 `protobuf:"varint,8,opt,name=charge,proto3" json:"charge,omitempty"`
+	MaximumIntegrity int32 `protobuf:"varint,4,opt,name=maximumIntegrity,proto3" json:"maximumIntegrity,omitempty"`
 	// The max energy the rover can store
-	MaximumCharge int32 `protobuf:"varint,9,opt,name=maximumCharge,proto3" json:"maximumCharge,omitempty"`
-	// The set of currently incoming commands for this tick
-	IncomingCommands []*Command `protobuf:"bytes,10,rep,name=incomingCommands,proto3" json:"incomingCommands,omitempty"`
+	MaximumCharge int32 `protobuf:"varint,5,opt,name=maximumCharge,proto3" json:"maximumCharge,omitempty"`
+}
+
+func (x *RoverSpecifications) Reset() {
+	*x = RoverSpecifications{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_roveapi_roveapi_proto_msgTypes[13]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RoverSpecifications) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RoverSpecifications) ProtoMessage() {}
+
+func (x *RoverSpecifications) ProtoReflect() protoreflect.Message {
+	mi := &file_roveapi_roveapi_proto_msgTypes[13]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RoverSpecifications.ProtoReflect.Descriptor instead.
+func (*RoverSpecifications) Descriptor() ([]byte, []int) {
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *RoverSpecifications) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *RoverSpecifications) GetRange() int32 {
+	if x != nil {
+		return x.Range
+	}
+	return 0
+}
+
+func (x *RoverSpecifications) GetCapacity() int32 {
+	if x != nil {
+		return x.Capacity
+	}
+	return 0
+}
+
+func (x *RoverSpecifications) GetMaximumIntegrity() int32 {
+	if x != nil {
+		return x.MaximumIntegrity
+	}
+	return 0
+}
+
+func (x *RoverSpecifications) GetMaximumCharge() int32 {
+	if x != nil {
+		return x.MaximumCharge
+	}
+	return 0
+}
+
+type RoverStatus struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The current direction of the rover
+	Bearing Bearing `protobuf:"varint,1,opt,name=bearing,proto3,enum=roveapi.Bearing" json:"bearing,omitempty"`
+	// The current position of the sails
+	SailPosition SailPosition `protobuf:"varint,2,opt,name=sailPosition,proto3,enum=roveapi.SailPosition" json:"sailPosition,omitempty"`
+	// The items in the rover inventory
+	Inventory []byte `protobuf:"bytes,3,opt,name=inventory,proto3" json:"inventory,omitempty"`
+	// The current health of the rover
+	Integrity int32 `protobuf:"varint,4,opt,name=integrity,proto3" json:"integrity,omitempty"`
+	// The energy stored in the rover
+	Charge int32 `protobuf:"varint,5,opt,name=charge,proto3" json:"charge,omitempty"`
 	// The set of currently queued commands
-	QueuedCommands []*Command `protobuf:"bytes,11,rep,name=queuedCommands,proto3" json:"queuedCommands,omitempty"`
+	QueuedCommands []*Command `protobuf:"bytes,6,rep,name=queuedCommands,proto3" json:"queuedCommands,omitempty"`
+}
+
+func (x *RoverStatus) Reset() {
+	*x = RoverStatus{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_roveapi_roveapi_proto_msgTypes[14]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RoverStatus) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RoverStatus) ProtoMessage() {}
+
+func (x *RoverStatus) ProtoReflect() protoreflect.Message {
+	mi := &file_roveapi_roveapi_proto_msgTypes[14]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RoverStatus.ProtoReflect.Descriptor instead.
+func (*RoverStatus) Descriptor() ([]byte, []int) {
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *RoverStatus) GetBearing() Bearing {
+	if x != nil {
+		return x.Bearing
+	}
+	return Bearing_BearingUnknown
+}
+
+func (x *RoverStatus) GetSailPosition() SailPosition {
+	if x != nil {
+		return x.SailPosition
+	}
+	return SailPosition_UnknownSailPosition
+}
+
+func (x *RoverStatus) GetInventory() []byte {
+	if x != nil {
+		return x.Inventory
+	}
+	return nil
+}
+
+func (x *RoverStatus) GetIntegrity() int32 {
+	if x != nil {
+		return x.Integrity
+	}
+	return 0
+}
+
+func (x *RoverStatus) GetCharge() int32 {
+	if x != nil {
+		return x.Charge
+	}
+	return 0
+}
+
+func (x *RoverStatus) GetQueuedCommands() []*Command {
+	if x != nil {
+		return x.QueuedCommands
+	}
+	return nil
+}
+
+type RoverReadings struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Position of the rover in world coordinates
+	Position *Vector `protobuf:"bytes,1,opt,name=position,proto3" json:"position,omitempty"`
+	// The current wind direction
+	Wind Bearing `protobuf:"varint,2,opt,name=wind,proto3,enum=roveapi.Bearing" json:"wind,omitempty"`
 	// The most recent logs
-	Logs []*Log `protobuf:"bytes,12,rep,name=logs,proto3" json:"logs,omitempty"`
+	Logs []*Log `protobuf:"bytes,3,rep,name=logs,proto3" json:"logs,omitempty"`
+}
+
+func (x *RoverReadings) Reset() {
+	*x = RoverReadings{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_roveapi_roveapi_proto_msgTypes[15]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RoverReadings) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RoverReadings) ProtoMessage() {}
+
+func (x *RoverReadings) ProtoReflect() protoreflect.Message {
+	mi := &file_roveapi_roveapi_proto_msgTypes[15]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RoverReadings.ProtoReflect.Descriptor instead.
+func (*RoverReadings) Descriptor() ([]byte, []int) {
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *RoverReadings) GetPosition() *Vector {
+	if x != nil {
+		return x.Position
+	}
+	return nil
+}
+
+func (x *RoverReadings) GetWind() Bearing {
+	if x != nil {
+		return x.Wind
+	}
+	return Bearing_BearingUnknown
+}
+
+func (x *RoverReadings) GetLogs() []*Log {
+	if x != nil {
+		return x.Logs
+	}
+	return nil
+}
+
+// StatusResponse is the response given to a status request
+type StatusResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The static rover information
+	Spec *RoverSpecifications `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"`
+	// Current rover status
+	Status *RoverStatus `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
+	// Current rover readings
+	Readings *RoverReadings `protobuf:"bytes,3,opt,name=readings,proto3" json:"readings,omitempty"`
 }
 
 func (x *StatusResponse) Reset() {
 	*x = StatusResponse{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_roveapi_roveapi_proto_msgTypes[13]
+		mi := &file_roveapi_roveapi_proto_msgTypes[16]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1067,7 +1429,7 @@ func (x *StatusResponse) String() string {
 func (*StatusResponse) ProtoMessage() {}
 
 func (x *StatusResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_roveapi_roveapi_proto_msgTypes[13]
+	mi := &file_roveapi_roveapi_proto_msgTypes[16]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1080,89 +1442,26 @@ func (x *StatusResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead.
 func (*StatusResponse) Descriptor() ([]byte, []int) {
-	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{13}
+	return file_roveapi_roveapi_proto_rawDescGZIP(), []int{16}
 }
 
-func (x *StatusResponse) GetName() string {
+func (x *StatusResponse) GetSpec() *RoverSpecifications {
 	if x != nil {
-		return x.Name
-	}
-	return ""
-}
-
-func (x *StatusResponse) GetPosition() *Vector {
-	if x != nil {
-		return x.Position
+		return x.Spec
 	}
 	return nil
 }
 
-func (x *StatusResponse) GetRange() int32 {
+func (x *StatusResponse) GetStatus() *RoverStatus {
 	if x != nil {
-		return x.Range
-	}
-	return 0
-}
-
-func (x *StatusResponse) GetInventory() []byte {
-	if x != nil {
-		return x.Inventory
+		return x.Status
 	}
 	return nil
 }
 
-func (x *StatusResponse) GetCapacity() int32 {
+func (x *StatusResponse) GetReadings() *RoverReadings {
 	if x != nil {
-		return x.Capacity
-	}
-	return 0
-}
-
-func (x *StatusResponse) GetIntegrity() int32 {
-	if x != nil {
-		return x.Integrity
-	}
-	return 0
-}
-
-func (x *StatusResponse) GetMaximumIntegrity() int32 {
-	if x != nil {
-		return x.MaximumIntegrity
-	}
-	return 0
-}
-
-func (x *StatusResponse) GetCharge() int32 {
-	if x != nil {
-		return x.Charge
-	}
-	return 0
-}
-
-func (x *StatusResponse) GetMaximumCharge() int32 {
-	if x != nil {
-		return x.MaximumCharge
-	}
-	return 0
-}
-
-func (x *StatusResponse) GetIncomingCommands() []*Command {
-	if x != nil {
-		return x.IncomingCommands
-	}
-	return nil
-}
-
-func (x *StatusResponse) GetQueuedCommands() []*Command {
-	if x != nil {
-		return x.QueuedCommands
-	}
-	return nil
-}
-
-func (x *StatusResponse) GetLogs() []*Log {
-	if x != nil {
-		return x.Logs
+		return x.Readings
 	}
 	return nil
 }
@@ -1193,115 +1492,157 @@ var file_roveapi_roveapi_proto_rawDesc = []byte{
 	0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a,
 	0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
 	0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
-	0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x8b, 0x01, 0x0a, 0x07, 0x43, 0x6f,
+	0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc2, 0x01, 0x0a, 0x07, 0x43, 0x6f,
 	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x2e, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
 	0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69,
 	0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x63, 0x6f,
-	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x2c, 0x0a, 0x07, 0x62, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69,
-	0x2e, 0x42, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x07, 0x62, 0x65, 0x61, 0x72,
-	0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03,
-	0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42,
-	0x06, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x6a, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x61,
-	0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x61, 0x63, 0x63,
-	0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76,
-	0x65, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x07, 0x61, 0x63,
-	0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
-	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70,
-	0x69, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61,
-	0x6e, 0x64, 0x73, 0x22, 0x11, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x0a, 0x0c, 0x52, 0x61, 0x64, 0x61, 0x72, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
-	0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70,
-	0x69, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75,
-	0x6e, 0x74, 0x22, 0x75, 0x0a, 0x0d, 0x52, 0x61, 0x64, 0x61, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x05, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x74, 0x69, 0x6c,
-	0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61,
-	0x70, 0x69, 0x2e, 0x54, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x74, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x29,
-	0x0a, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32,
-	0x0f, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
-	0x52, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x22, 0x3b, 0x0a, 0x0d, 0x53, 0x74, 0x61,
-	0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x07, 0x61, 0x63,
-	0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f,
-	0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x07, 0x61,
-	0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2d, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x12, 0x0a,
-	0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x69, 0x6d,
-	0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x24, 0x0a, 0x06, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12,
-	0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a,
-	0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x22, 0xc3, 0x03, 0x0a, 0x0e,
-	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12,
-	0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
-	0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x56,
-	0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12,
-	0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
-	0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f,
-	0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x69, 0x6e, 0x76, 0x65, 0x6e, 0x74,
-	0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x18,
-	0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79, 0x12,
-	0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01,
-	0x28, 0x05, 0x52, 0x09, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x12, 0x2a, 0x0a,
-	0x10, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74,
-	0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d,
-	0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x68, 0x61,
-	0x72, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x63, 0x68, 0x61, 0x72, 0x67,
-	0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x43, 0x68, 0x61, 0x72,
-	0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75,
-	0x6d, 0x43, 0x68, 0x61, 0x72, 0x67, 0x65, 0x12, 0x3c, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6f, 0x6d,
-	0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28,
-	0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6d, 0x6d,
-	0x61, 0x6e, 0x64, 0x52, 0x10, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d,
-	0x6d, 0x61, 0x6e, 0x64, 0x73, 0x12, 0x38, 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x43,
-	0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e,
-	0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52,
-	0x0e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x12,
-	0x20, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e,
-	0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67,
-	0x73, 0x2a, 0x55, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65,
-	0x12, 0x08, 0x0a, 0x04, 0x6e, 0x6f, 0x6e, 0x65, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x6d, 0x6f,
-	0x76, 0x65, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x73, 0x68, 0x10, 0x02, 0x12,
-	0x0a, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x61, 0x69, 0x72, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x72,
-	0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x62, 0x72, 0x6f,
-	0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x10, 0x05, 0x2a, 0x47, 0x0a, 0x07, 0x42, 0x65, 0x61, 0x72,
-	0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x0e, 0x42, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x6e,
-	0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4e, 0x6f, 0x72, 0x74, 0x68,
-	0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x45, 0x61, 0x73, 0x74, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05,
-	0x53, 0x6f, 0x75, 0x74, 0x68, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x65, 0x73, 0x74, 0x10,
-	0x04, 0x2a, 0x48, 0x0a, 0x06, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x11, 0x0a, 0x0d, 0x4f,
-	0x62, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x0d,
-	0x0a, 0x09, 0x52, 0x6f, 0x76, 0x65, 0x72, 0x4c, 0x69, 0x76, 0x65, 0x10, 0x01, 0x12, 0x0d, 0x0a,
-	0x09, 0x52, 0x6f, 0x63, 0x6b, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09,
-	0x52, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x72, 0x67, 0x65, 0x10, 0x03, 0x2a, 0x37, 0x0a, 0x04, 0x54,
-	0x69, 0x6c, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x54, 0x69, 0x6c, 0x65, 0x55, 0x6e, 0x6b, 0x6e, 0x6f,
-	0x77, 0x6e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x6f, 0x63, 0x6b, 0x10, 0x01, 0x12, 0x0a,
-	0x0a, 0x06, 0x47, 0x72, 0x61, 0x76, 0x65, 0x6c, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x61,
-	0x6e, 0x64, 0x10, 0x03, 0x32, 0xcf, 0x02, 0x0a, 0x04, 0x52, 0x6f, 0x76, 0x65, 0x12, 0x4d, 0x0a,
-	0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x2e,
-	0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74,
-	0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f,
-	0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
-	0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x08,
-	0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61,
-	0x70, 0x69, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x67,
-	0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
-	0x3e, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x17, 0x2e, 0x72, 0x6f, 0x76,
-	0x65, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f,
-	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
-	0x38, 0x0a, 0x05, 0x52, 0x61, 0x64, 0x61, 0x72, 0x12, 0x15, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61,
-	0x70, 0x69, 0x2e, 0x52, 0x61, 0x64, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x16, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x61, 0x64, 0x61, 0x72, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x53, 0x74, 0x61,
-	0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74,
-	0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x72, 0x6f,
-	0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
-	0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x64, 0x69, 0x6c, 0x75, 0x7a, 0x2f, 0x72, 0x6f, 0x76, 0x65,
-	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x62, 0x06,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x12, 0x12, 0x0a,
+	0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74,
+	0x61, 0x12, 0x2a, 0x0a, 0x07, 0x62, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01,
+	0x28, 0x0e, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x42, 0x65, 0x61,
+	0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x62, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x2f, 0x0a,
+	0x07, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15,
+	0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x6f, 0x76, 0x65, 0x72, 0x55, 0x70,
+	0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x22, 0x6a,
+	0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x12, 0x2a, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x63, 0x63, 0x6f,
+	0x75, 0x6e, 0x74, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x08,
+	0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10,
+	0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
+	0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x22, 0x11, 0x0a, 0x0f, 0x43, 0x6f,
+	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x0a,
+	0x0c, 0x52, 0x61, 0x64, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a,
+	0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
+	0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
+	0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x75, 0x0a, 0x0d, 0x52, 0x61, 0x64,
+	0x61, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61,
+	0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65,
+	0x12, 0x23, 0x0a, 0x05, 0x74, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32,
+	0x0d, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x69, 0x6c, 0x65, 0x52, 0x05,
+	0x74, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73,
+	0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69,
+	0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73,
+	0x22, 0x3b, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x12, 0x2a, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x63, 0x63,
+	0x6f, 0x75, 0x6e, 0x74, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2d, 0x0a,
+	0x03, 0x4c, 0x6f, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x24, 0x0a, 0x06,
+	0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52,
+	0x01, 0x79, 0x22, 0xad, 0x01, 0x0a, 0x13, 0x52, 0x6f, 0x76, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63,
+	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14,
+	0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x72,
+	0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x63, 0x61, 0x70, 0x61, 0x63, 0x69, 0x74, 0x79,
+	0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x49, 0x6e, 0x74, 0x65, 0x67,
+	0x72, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x69,
+	0x6d, 0x75, 0x6d, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x12, 0x24, 0x0a, 0x0d,
+	0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x43, 0x68, 0x61, 0x72, 0x67, 0x65, 0x18, 0x05, 0x20,
+	0x01, 0x28, 0x05, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x43, 0x68, 0x61, 0x72,
+	0x67, 0x65, 0x22, 0x82, 0x02, 0x0a, 0x0b, 0x52, 0x6f, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
+	0x75, 0x73, 0x12, 0x2a, 0x0a, 0x07, 0x62, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x42, 0x65,
+	0x61, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x62, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x39,
+	0x0a, 0x0c, 0x73, 0x61, 0x69, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x53,
+	0x61, 0x69, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x61, 0x69,
+	0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x76,
+	0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x69, 0x6e,
+	0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x65, 0x67,
+	0x72, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x69, 0x6e, 0x74, 0x65,
+	0x67, 0x72, 0x69, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x12, 0x38, 0x0a,
+	0x0e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x18,
+	0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e,
+	0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x0e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x64, 0x43,
+	0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x22, 0x84, 0x01, 0x0a, 0x0d, 0x52, 0x6f, 0x76, 0x65,
+	0x72, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2b, 0x0a, 0x08, 0x70, 0x6f, 0x73,
+	0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x72, 0x6f,
+	0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x08, 0x70, 0x6f,
+	0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x04, 0x77, 0x69, 0x6e, 0x64, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x42,
+	0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x04, 0x77, 0x69, 0x6e, 0x64, 0x12, 0x20, 0x0a, 0x04,
+	0x6c, 0x6f, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x72, 0x6f, 0x76,
+	0x65, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x22, 0xa4,
+	0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1c, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x6f, 0x76, 0x65, 0x72, 0x53,
+	0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x04, 0x73,
+	0x70, 0x65, 0x63, 0x12, 0x2c, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x6f,
+	0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+	0x73, 0x12, 0x32, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x6f,
+	0x76, 0x65, 0x72, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x72, 0x65, 0x61,
+	0x64, 0x69, 0x6e, 0x67, 0x73, 0x2a, 0x85, 0x01, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
+	0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x6e, 0x6f, 0x6e, 0x65, 0x10, 0x00, 0x12,
+	0x08, 0x0a, 0x04, 0x77, 0x61, 0x69, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x74, 0x6f, 0x67,
+	0x67, 0x6c, 0x65, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x74, 0x75, 0x72, 0x6e, 0x10, 0x03, 0x12,
+	0x09, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x73, 0x68, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x72, 0x65,
+	0x70, 0x61, 0x69, 0x72, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63,
+	0x61, 0x73, 0x74, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, 0x73, 0x61, 0x6c, 0x76, 0x61, 0x67, 0x65,
+	0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x10, 0x08,
+	0x12, 0x0b, 0x0a, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x10, 0x09, 0x2a, 0x83, 0x01,
+	0x0a, 0x07, 0x42, 0x65, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x0e, 0x42, 0x65, 0x61,
+	0x72, 0x69, 0x6e, 0x67, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a,
+	0x05, 0x4e, 0x6f, 0x72, 0x74, 0x68, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x6f, 0x72, 0x74,
+	0x68, 0x45, 0x61, 0x73, 0x74, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x45, 0x61, 0x73, 0x74, 0x10,
+	0x03, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x6f, 0x75, 0x74, 0x68, 0x45, 0x61, 0x73, 0x74, 0x10, 0x04,
+	0x12, 0x09, 0x0a, 0x05, 0x53, 0x6f, 0x75, 0x74, 0x68, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x53,
+	0x6f, 0x75, 0x74, 0x68, 0x57, 0x65, 0x73, 0x74, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x65,
+	0x73, 0x74, 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x6f, 0x72, 0x74, 0x68, 0x57, 0x65, 0x73,
+	0x74, 0x10, 0x08, 0x2a, 0x69, 0x0a, 0x0c, 0x52, 0x6f, 0x76, 0x65, 0x72, 0x55, 0x70, 0x67, 0x72,
+	0x61, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x6f, 0x76, 0x65, 0x72, 0x55, 0x70, 0x67, 0x72,
+	0x61, 0x64, 0x65, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05,
+	0x52, 0x61, 0x6e, 0x67, 0x65, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x61, 0x70, 0x61, 0x63,
+	0x69, 0x74, 0x79, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d,
+	0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x4d,
+	0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x43, 0x68, 0x61, 0x72, 0x67, 0x65, 0x10, 0x04, 0x2a, 0x6a,
+	0x0a, 0x06, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x11, 0x0a, 0x0d, 0x4f, 0x62, 0x6a, 0x65,
+	0x63, 0x74, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x52,
+	0x6f, 0x76, 0x65, 0x72, 0x4c, 0x69, 0x76, 0x65, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x6f,
+	0x76, 0x65, 0x72, 0x44, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x74, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09,
+	0x52, 0x6f, 0x63, 0x6b, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x52,
+	0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x72, 0x67, 0x65, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x6f,
+	0x76, 0x65, 0x72, 0x50, 0x61, 0x72, 0x74, 0x73, 0x10, 0x05, 0x2a, 0x37, 0x0a, 0x04, 0x54, 0x69,
+	0x6c, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x54, 0x69, 0x6c, 0x65, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77,
+	0x6e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x6f, 0x63, 0x6b, 0x10, 0x01, 0x12, 0x0a, 0x0a,
+	0x06, 0x47, 0x72, 0x61, 0x76, 0x65, 0x6c, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x61, 0x6e,
+	0x64, 0x10, 0x03, 0x2a, 0x4c, 0x0a, 0x0c, 0x53, 0x61, 0x69, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74,
+	0x69, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x53, 0x61,
+	0x69, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c,
+	0x43, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x57, 0x69, 0x6e, 0x64, 0x10, 0x01, 0x12, 0x11,
+	0x0a, 0x0d, 0x53, 0x6f, 0x6c, 0x61, 0x72, 0x43, 0x68, 0x61, 0x72, 0x67, 0x69, 0x6e, 0x67, 0x10,
+	0x02, 0x32, 0xcf, 0x02, 0x0a, 0x04, 0x52, 0x6f, 0x76, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x53, 0x65,
+	0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x2e, 0x72, 0x6f, 0x76,
+	0x65, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75,
+	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61,
+	0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x08, 0x52, 0x65, 0x67,
+	0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e,
+	0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+	0x19, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74,
+	0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x07,
+	0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x17, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70,
+	0x69, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x1a, 0x18, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61,
+	0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x05,
+	0x52, 0x61, 0x64, 0x61, 0x72, 0x12, 0x15, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e,
+	0x52, 0x61, 0x64, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72,
+	0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x61, 0x64, 0x61, 0x72, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
+	0x12, 0x16, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75,
+	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x72, 0x6f, 0x76, 0x65, 0x61,
+	0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x22, 0x00, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+	0x6d, 0x2f, 0x6d, 0x64, 0x69, 0x6c, 0x75, 0x7a, 0x2f, 0x72, 0x6f, 0x76, 0x65, 0x2f, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x6f, 0x76, 0x65, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1316,57 +1657,68 @@ func file_roveapi_roveapi_proto_rawDescGZIP() []byte {
 	return file_roveapi_roveapi_proto_rawDescData
 }
 
-var file_roveapi_roveapi_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
-var file_roveapi_roveapi_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
+var file_roveapi_roveapi_proto_enumTypes = make([]protoimpl.EnumInfo, 6)
+var file_roveapi_roveapi_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
 var file_roveapi_roveapi_proto_goTypes = []interface{}{
 	(CommandType)(0),             // 0: roveapi.CommandType
 	(Bearing)(0),                 // 1: roveapi.Bearing
-	(Object)(0),                  // 2: roveapi.Object
-	(Tile)(0),                    // 3: roveapi.Tile
-	(*ServerStatusRequest)(nil),  // 4: roveapi.ServerStatusRequest
-	(*ServerStatusResponse)(nil), // 5: roveapi.ServerStatusResponse
-	(*RegisterRequest)(nil),      // 6: roveapi.RegisterRequest
-	(*Account)(nil),              // 7: roveapi.Account
-	(*RegisterResponse)(nil),     // 8: roveapi.RegisterResponse
-	(*Command)(nil),              // 9: roveapi.Command
-	(*CommandRequest)(nil),       // 10: roveapi.CommandRequest
-	(*CommandResponse)(nil),      // 11: roveapi.CommandResponse
-	(*RadarRequest)(nil),         // 12: roveapi.RadarRequest
-	(*RadarResponse)(nil),        // 13: roveapi.RadarResponse
-	(*StatusRequest)(nil),        // 14: roveapi.StatusRequest
-	(*Log)(nil),                  // 15: roveapi.Log
-	(*Vector)(nil),               // 16: roveapi.Vector
-	(*StatusResponse)(nil),       // 17: roveapi.StatusResponse
+	(RoverUpgrade)(0),            // 2: roveapi.RoverUpgrade
+	(Object)(0),                  // 3: roveapi.Object
+	(Tile)(0),                    // 4: roveapi.Tile
+	(SailPosition)(0),            // 5: roveapi.SailPosition
+	(*ServerStatusRequest)(nil),  // 6: roveapi.ServerStatusRequest
+	(*ServerStatusResponse)(nil), // 7: roveapi.ServerStatusResponse
+	(*RegisterRequest)(nil),      // 8: roveapi.RegisterRequest
+	(*Account)(nil),              // 9: roveapi.Account
+	(*RegisterResponse)(nil),     // 10: roveapi.RegisterResponse
+	(*Command)(nil),              // 11: roveapi.Command
+	(*CommandRequest)(nil),       // 12: roveapi.CommandRequest
+	(*CommandResponse)(nil),      // 13: roveapi.CommandResponse
+	(*RadarRequest)(nil),         // 14: roveapi.RadarRequest
+	(*RadarResponse)(nil),        // 15: roveapi.RadarResponse
+	(*StatusRequest)(nil),        // 16: roveapi.StatusRequest
+	(*Log)(nil),                  // 17: roveapi.Log
+	(*Vector)(nil),               // 18: roveapi.Vector
+	(*RoverSpecifications)(nil),  // 19: roveapi.RoverSpecifications
+	(*RoverStatus)(nil),          // 20: roveapi.RoverStatus
+	(*RoverReadings)(nil),        // 21: roveapi.RoverReadings
+	(*StatusResponse)(nil),       // 22: roveapi.StatusResponse
 }
 var file_roveapi_roveapi_proto_depIdxs = []int32{
-	7,  // 0: roveapi.RegisterResponse.account:type_name -> roveapi.Account
+	9,  // 0: roveapi.RegisterResponse.account:type_name -> roveapi.Account
 	0,  // 1: roveapi.Command.command:type_name -> roveapi.CommandType
 	1,  // 2: roveapi.Command.bearing:type_name -> roveapi.Bearing
-	7,  // 3: roveapi.CommandRequest.account:type_name -> roveapi.Account
-	9,  // 4: roveapi.CommandRequest.commands:type_name -> roveapi.Command
-	7,  // 5: roveapi.RadarRequest.account:type_name -> roveapi.Account
-	3,  // 6: roveapi.RadarResponse.tiles:type_name -> roveapi.Tile
-	2,  // 7: roveapi.RadarResponse.objects:type_name -> roveapi.Object
-	7,  // 8: roveapi.StatusRequest.account:type_name -> roveapi.Account
-	16, // 9: roveapi.StatusResponse.position:type_name -> roveapi.Vector
-	9,  // 10: roveapi.StatusResponse.incomingCommands:type_name -> roveapi.Command
-	9,  // 11: roveapi.StatusResponse.queuedCommands:type_name -> roveapi.Command
-	15, // 12: roveapi.StatusResponse.logs:type_name -> roveapi.Log
-	4,  // 13: roveapi.Rove.ServerStatus:input_type -> roveapi.ServerStatusRequest
-	6,  // 14: roveapi.Rove.Register:input_type -> roveapi.RegisterRequest
-	10, // 15: roveapi.Rove.Command:input_type -> roveapi.CommandRequest
-	12, // 16: roveapi.Rove.Radar:input_type -> roveapi.RadarRequest
-	14, // 17: roveapi.Rove.Status:input_type -> roveapi.StatusRequest
-	5,  // 18: roveapi.Rove.ServerStatus:output_type -> roveapi.ServerStatusResponse
-	8,  // 19: roveapi.Rove.Register:output_type -> roveapi.RegisterResponse
-	11, // 20: roveapi.Rove.Command:output_type -> roveapi.CommandResponse
-	13, // 21: roveapi.Rove.Radar:output_type -> roveapi.RadarResponse
-	17, // 22: roveapi.Rove.Status:output_type -> roveapi.StatusResponse
-	18, // [18:23] is the sub-list for method output_type
-	13, // [13:18] is the sub-list for method input_type
-	13, // [13:13] is the sub-list for extension type_name
-	13, // [13:13] is the sub-list for extension extendee
-	0,  // [0:13] is the sub-list for field type_name
+	2,  // 3: roveapi.Command.upgrade:type_name -> roveapi.RoverUpgrade
+	9,  // 4: roveapi.CommandRequest.account:type_name -> roveapi.Account
+	11, // 5: roveapi.CommandRequest.commands:type_name -> roveapi.Command
+	9,  // 6: roveapi.RadarRequest.account:type_name -> roveapi.Account
+	4,  // 7: roveapi.RadarResponse.tiles:type_name -> roveapi.Tile
+	3,  // 8: roveapi.RadarResponse.objects:type_name -> roveapi.Object
+	9,  // 9: roveapi.StatusRequest.account:type_name -> roveapi.Account
+	1,  // 10: roveapi.RoverStatus.bearing:type_name -> roveapi.Bearing
+	5,  // 11: roveapi.RoverStatus.sailPosition:type_name -> roveapi.SailPosition
+	11, // 12: roveapi.RoverStatus.queuedCommands:type_name -> roveapi.Command
+	18, // 13: roveapi.RoverReadings.position:type_name -> roveapi.Vector
+	1,  // 14: roveapi.RoverReadings.wind:type_name -> roveapi.Bearing
+	17, // 15: roveapi.RoverReadings.logs:type_name -> roveapi.Log
+	19, // 16: roveapi.StatusResponse.spec:type_name -> roveapi.RoverSpecifications
+	20, // 17: roveapi.StatusResponse.status:type_name -> roveapi.RoverStatus
+	21, // 18: roveapi.StatusResponse.readings:type_name -> roveapi.RoverReadings
+	6,  // 19: roveapi.Rove.ServerStatus:input_type -> roveapi.ServerStatusRequest
+	8,  // 20: roveapi.Rove.Register:input_type -> roveapi.RegisterRequest
+	12, // 21: roveapi.Rove.Command:input_type -> roveapi.CommandRequest
+	14, // 22: roveapi.Rove.Radar:input_type -> roveapi.RadarRequest
+	16, // 23: roveapi.Rove.Status:input_type -> roveapi.StatusRequest
+	7,  // 24: roveapi.Rove.ServerStatus:output_type -> roveapi.ServerStatusResponse
+	10, // 25: roveapi.Rove.Register:output_type -> roveapi.RegisterResponse
+	13, // 26: roveapi.Rove.Command:output_type -> roveapi.CommandResponse
+	15, // 27: roveapi.Rove.Radar:output_type -> roveapi.RadarResponse
+	22, // 28: roveapi.Rove.Status:output_type -> roveapi.StatusResponse
+	24, // [24:29] is the sub-list for method output_type
+	19, // [19:24] is the sub-list for method input_type
+	19, // [19:19] is the sub-list for extension type_name
+	19, // [19:19] is the sub-list for extension extendee
+	0,  // [0:19] is the sub-list for field type_name
 }
 
 func init() { file_roveapi_roveapi_proto_init() }
@@ -1532,6 +1884,42 @@ func file_roveapi_roveapi_proto_init() {
 			}
 		}
 		file_roveapi_roveapi_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RoverSpecifications); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_roveapi_roveapi_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RoverStatus); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_roveapi_roveapi_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RoverReadings); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_roveapi_roveapi_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*StatusResponse); i {
 			case 0:
 				return &v.state
@@ -1544,17 +1932,13 @@ func file_roveapi_roveapi_proto_init() {
 			}
 		}
 	}
-	file_roveapi_roveapi_proto_msgTypes[5].OneofWrappers = []interface{}{
-		(*Command_Bearing)(nil),
-		(*Command_Message)(nil),
-	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_roveapi_roveapi_proto_rawDesc,
-			NumEnums:      4,
-			NumMessages:   14,
+			NumEnums:      6,
+			NumMessages:   17,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/proto/roveapi/roveapi.proto b/proto/roveapi/roveapi.proto
index b43087b..72a1fc2 100644
--- a/proto/roveapi/roveapi.proto
+++ b/proto/roveapi/roveapi.proto
@@ -89,25 +89,47 @@ message RegisterResponse {
 // CommandType defines the type of a command to give to the rover
 enum CommandType {
   none = 0;
-  // Move the rover in a direction, requires bearing
-  move = 1;
+  // Waits before performing the next command
+  wait = 1;
+  // Toggles the sails, either catching the wind, or charging from the sun
+  toggle = 2;
+  // Turns the rover in the specified bearing (requires bearing)
+  turn = 3;
   // Stashes item at current location in rover inventory
-  stash = 2;
+  stash = 4;
   // Repairs the rover using an inventory object
-  repair = 3;
-  // Waits a tick to add more charge to the rover
-  recharge = 4;
-  // Broadcasts a message to nearby rovers
-  broadcast = 5;
+  repair = 5;
+  // Broadcasts a message to nearby rovers (requires data)
+  broadcast = 6;
+  // Salvages a neighboring dormant rover for parts
+  salvage = 7;
+  // Transfers remote control into dormant rover
+  transfer = 8;
+  // Upgrades a chosen rover specification using 5 rover parts
+  upgrade = 9;
 }
 
+// Bearing represents a compass direction
 enum Bearing {
   // BearingUnknown an unknown invalid bearing
   BearingUnknown = 0;
   North = 1;
-  East = 2;
-  South = 3;
-  West = 4;
+  NorthEast = 2;
+  East = 3;
+  SouthEast = 4;
+  South = 5;
+  SouthWest = 6;
+  West = 7;
+  NorthWest = 8;
+}
+
+// Describes the type of upgrade
+enum RoverUpgrade {
+  RoverUpgradeUnknown = 0;
+  Range = 1;
+  Capacity = 2;
+  MaximumIntegrity = 3;
+  MaximumCharge = 4;
 }
 
 // Command is a single command for a rover
@@ -115,16 +137,18 @@ message Command {
   // The command type
   CommandType command = 1;
 
-  oneof data {
-    // A bearing
-    // Used with MOVE
-    Bearing bearing = 2;
+  // The number of times to repeat the command after the first
+  int32 repeat = 2;
 
-    // A simple message, must be composed of printable ASCII glyphs (32-126)
-    // maximum of three characters
-    // Used with BROADCAST
-    bytes message = 3;
-  }
+  // broadcast - a simple message, must be composed of up to 3 printable ASCII
+  // glyphs (32-126)
+  bytes data = 3;
+
+  // move - the bearing for the rover to turn to
+  Bearing bearing = 4;
+
+  // upgrade - the upgrade to apply to the rover
+  RoverUpgrade upgrade = 5;
 }
 
 // CommandRequest describes a set of commands to be requested for the rover
@@ -151,11 +175,18 @@ enum Object {
   // RoverLive represents a live rover
   RoverLive = 1;
 
+  // RoverDormant describes a dormant rover
+  RoverDormant = 2;
+
   // RockSmall is a small stashable rock
-  RockSmall = 2;
+  RockSmall = 3;
 
   // RockLarge is a large blocking rock
-  RockLarge = 3;
+  RockLarge = 4;
+
+  // RoverParts is one unit of rover parts, used for repairing and fixing the
+  // rover
+  RoverParts = 5;
 }
 
 enum Tile {
@@ -216,41 +247,76 @@ message Vector {
   int32 y = 2;
 }
 
-// StatusResponse is the response given to a status request
-message StatusResponse {
+// SailPosition represents the position of the sola sail
+enum SailPosition {
+  UnknownSailPosition = 0;
+
+  // CatchingWind means the sail is catching the wind and moving the rover
+  CatchingWind = 1;
+
+  // SolarCharging means the sail is facing the sun and charging
+  SolarCharging = 2;
+}
+
+message RoverSpecifications {
+
   // The name of the rover
   string name = 1;
 
-  // Position of the rover in world coordinates
-  Vector position = 2;
-
   // The range of this rover's radar and broadcasting
-  int32 range = 3;
-
-  // The items in the rover inventory
-  bytes inventory = 4;
+  int32 range = 2;
 
   // The capacity of the inventory
-  int32 capacity = 5;
-
-  // The current health of the rover
-  int32 integrity = 6;
+  int32 capacity = 3;
 
   // The maximum health of the rover
-  int32 maximumIntegrity = 7;
-
-  // The energy stored in the rover
-  int32 charge = 8;
+  int32 maximumIntegrity = 4;
 
   // The max energy the rover can store
-  int32 maximumCharge = 9;
+  int32 maximumCharge = 5;
+}
 
-  // The set of currently incoming commands for this tick
-  repeated Command incomingCommands = 10;
+message RoverStatus {
+
+  // The current direction of the rover
+  Bearing bearing = 1;
+
+  // The current position of the sails
+  SailPosition sailPosition = 2;
+
+  // The items in the rover inventory
+  bytes inventory = 3;
+
+  // The current health of the rover
+  int32 integrity = 4;
+
+  // The energy stored in the rover
+  int32 charge = 5;
 
   // The set of currently queued commands
-  repeated Command queuedCommands = 11;
+  repeated Command queuedCommands = 6;
+}
+
+message RoverReadings {
+  // Position of the rover in world coordinates
+  Vector position = 1;
+
+  // The current wind direction
+  Bearing wind = 2;
 
   // The most recent logs
-  repeated Log logs = 12;
+  repeated Log logs = 3;
+}
+
+// StatusResponse is the response given to a status request
+message StatusResponse {
+
+  // The static rover information
+  RoverSpecifications spec = 1;
+
+  // Current rover status
+  RoverStatus status = 2;
+
+  // Current rover readings
+  RoverReadings readings = 3;
 }
\ No newline at end of file