-
-
Notifications
You must be signed in to change notification settings - Fork 155
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
588 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Satisfactory Save Sharing | ||
|
||
**_Note: this is a work in progress. The group I play with have been relying on solely this for the last few months, but I'm still working on making it more user-friendly._** | ||
|
||
The dedicated server for Satisfactory introduces a few unique bugs to the game, where multiplayer (through joining a friend) doesn't. This application introduces save sharing with friends. It's designed to function similarly to how the game Grounded handles saves. | ||
|
||
Everybody runs the client in the background; when the host's game saves, those files are uploaded to a remote SFTP server (deployed through the Docker Compose below), which the other clients pull from in realtime. This way, if the host leaves, anyone else can pick up from where they left off. | ||
|
||
## Setup | ||
|
||
Download the release from the releases tab. When you initially run it, it'll ask for the following information: | ||
- Server address (IP and port, e.g. `localhost:15770`) | ||
- Server password (the SFTP password) | ||
- Session name (this must be EXACTLY as it is formatted within Satisfactory) | ||
|
||
### Docker Compose | ||
|
||
If you're using [Docker Compose](https://docs.docker.com/compose/): | ||
|
||
```yaml | ||
version: '3' | ||
services: | ||
satisfactory-saveshare: | ||
container_name: satisfactory-saveshare | ||
image: atmoz/sftp:latest | ||
volumes: | ||
- /opt/saveshare:/home/saveshare/upload | ||
ports: | ||
- "15770:22" | ||
command: saveshare:PASSWORD_HERE:1001 | ||
``` | ||
_Note: Do not change the username (`saveshare`) or the UID (`1001`). Only change the password._ | ||
|
||
### Known Issues | ||
You can't delete blueprints, unless you manually stop everyone from running the application and everyone deletes the blueprint locally (and server-side) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package main | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
type Config struct { | ||
gamePath string | ||
path string | ||
BlueprintPath string `yaml:"blueprintPath"` | ||
SavePath string `yaml:"savePath"` | ||
ServerAddress string `yaml:"serverAddress"` | ||
ServerPassword string `yaml:"serverPassword"` | ||
SessionName string `yaml:"sessionName"` | ||
} | ||
|
||
func NewConfig(configDir string) (*Config, error) { | ||
cfg := Config{ | ||
path: configDir + slash + "config.yml", | ||
} | ||
|
||
// Check if the file exists | ||
if _, err := os.Stat(cfg.path); os.IsNotExist(err) { | ||
// If the file doesn't exist, create an empty file | ||
if _, err = os.Create(cfg.path); err != nil { | ||
return nil, fmt.Errorf("could not create config file: %w", err) | ||
} | ||
} | ||
|
||
// Read the YAML file | ||
yamlData, err := os.ReadFile(cfg.path) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not read config file: %w", err) | ||
} | ||
|
||
if err = yaml.Unmarshal(yamlData, &cfg); err != nil { | ||
return nil, fmt.Errorf("could not unmarshal config file: %w", err) | ||
} | ||
|
||
// populate the blueprint and save paths | ||
appDataPath, err := os.UserCacheDir() | ||
if err != nil { | ||
return nil, fmt.Errorf("could not get appdata path: %w", err) | ||
} | ||
|
||
cfg.gamePath = appDataPath + slash + "FactoryGame" + slash + "Saved" + slash + "SaveGames" + slash | ||
|
||
if cfg.SessionName != "" { | ||
cfg.BlueprintPath = cfg.gamePath + "blueprints" + slash + cfg.SessionName | ||
} | ||
|
||
// check if the game path exists | ||
if _, err = os.Stat(cfg.gamePath); os.IsNotExist(err) { | ||
return nil, fmt.Errorf("game path does not exist: %w", err) | ||
} | ||
|
||
// get the save path | ||
if err = filepath.Walk(cfg.gamePath, func(path string, info os.FileInfo, err error) error { | ||
if info == nil { | ||
return errors.New("path does not exist") | ||
} | ||
|
||
if info.IsDir() { | ||
if cfg.SavePath == "" && path != cfg.gamePath { | ||
cfg.SavePath = path | ||
} | ||
return nil | ||
} | ||
|
||
return nil | ||
}); err != nil { | ||
return nil, fmt.Errorf("could not walk over path: %w", err) | ||
} | ||
|
||
if cfg.SavePath == "" { | ||
return nil, fmt.Errorf("could not find save path") | ||
} | ||
|
||
return &cfg, nil | ||
} | ||
|
||
func (c *Config) Save() error { | ||
yamlData, err := yaml.Marshal(c) | ||
if err != nil { | ||
return fmt.Errorf("could not marshal config file: %w", err) | ||
} | ||
|
||
if err = os.WriteFile(c.path, yamlData, 0o644); err != nil { | ||
return fmt.Errorf("could not write config file: %w", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
version: '3' | ||
services: | ||
satisfactory-saveshare: | ||
container_name: satisfactory-saveshare | ||
image: atmoz/sftp:latest | ||
volumes: | ||
- /opt/saveshare:/home/saveshare/upload | ||
ports: | ||
- "15770:22" | ||
command: saveshare:PASSWORD_HERE:1001 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
|
||
"github.com/pkg/sftp" | ||
) | ||
|
||
func downloadFile(sftpClient *sftp.Client, remotePath, localPath string) error { | ||
remoteFile, err := sftpClient.Open(remotePath) | ||
if err != nil { | ||
return fmt.Errorf("error opening remote file: %v: %w", remotePath, err) | ||
} | ||
defer remoteFile.Close() | ||
|
||
localFile, err := os.Create(localPath) | ||
if err != nil { | ||
return fmt.Errorf("error creating local file: %w", err) | ||
} | ||
defer localFile.Close() | ||
|
||
if _, err = io.Copy(localFile, remoteFile); err != nil { | ||
return fmt.Errorf("error copying file: %w", err) | ||
} | ||
|
||
fileInfo, err := remoteFile.Stat() | ||
if err != nil { | ||
return fmt.Errorf("error getting remote file info: %w", err) | ||
} | ||
|
||
if err = os.Chtimes(localPath, fileInfo.ModTime(), fileInfo.ModTime()); err != nil { | ||
return fmt.Errorf("error setting local file mod time: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func uploadFile(sftpClient *sftp.Client, localPath, remotePath string) error { | ||
localFile, err := os.Open(localPath) | ||
if err != nil { | ||
return fmt.Errorf("error opening local file: %w", err) | ||
} | ||
defer localFile.Close() | ||
|
||
remoteFile, err := sftpClient.Create(remotePath) | ||
if err != nil { | ||
return fmt.Errorf("error creating remote file: %w", err) | ||
} | ||
defer remoteFile.Close() | ||
|
||
if _, err = io.Copy(remoteFile, localFile); err != nil { | ||
return fmt.Errorf("error copying file: %w", err) | ||
} | ||
|
||
fileInfo, err := localFile.Stat() | ||
if err != nil { | ||
return fmt.Errorf("error getting local file info: %w", err) | ||
} | ||
|
||
if err = sftpClient.Chtimes(remotePath, fileInfo.ModTime(), fileInfo.ModTime()); err != nil { | ||
return fmt.Errorf("error setting remote file mod time: %w", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module github.com/wolveix/satisfactory-server/saveshare | ||
|
||
go 1.20 | ||
|
||
require ( | ||
github.com/pkg/sftp v1.13.6 | ||
github.com/rs/zerolog v1.30.0 | ||
golang.org/x/crypto v0.13.0 | ||
gopkg.in/yaml.v3 v3.0.1 | ||
) | ||
|
||
require ( | ||
github.com/kr/fs v0.1.0 // indirect | ||
github.com/mattn/go-colorable v0.1.12 // indirect | ||
github.com/mattn/go-isatty v0.0.14 // indirect | ||
golang.org/x/sys v0.12.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= | ||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= | ||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= | ||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | ||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= | ||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= | ||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= | ||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= | ||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= | ||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= | ||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= | ||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= | ||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= | ||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= | ||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
Oops, something went wrong.