Skip to content

Commit e16d18b

Browse files
committed
Release SaveShare
1 parent 4557bb3 commit e16d18b

File tree

10 files changed

+588
-3
lines changed

10 files changed

+588
-3
lines changed

.github/workflows/docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@ jobs:
3838
with:
3939
push: true
4040
tags: |
41-
ghcr.io/${{ github.repository }}/${{ github.event.repository.name }}:dev
41+
ghcr.io/${{ github.repository }}:dev
4242
${{ steps.meta.outputs.tags }}

.github/workflows/release.yml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
# list of Docker images to use as base name for tags
3030
images: |
3131
${{ github.repository }}
32-
ghcr.io/${{ github.repository }}/${{ github.event.repository.name }}
32+
ghcr.io/${{ github.repository }}
3333
# generate Docker tags based on the following events/attributes
3434
tags: |
3535
type=semver,pattern=v{{version}}
@@ -44,4 +44,23 @@ jobs:
4444
uses: docker/build-push-action@v2
4545
with:
4646
push: true
47-
tags: ${{ steps.meta.outputs.tags }}
47+
tags: ${{ steps.meta.outputs.tags }}
48+
49+
- name: Setup Go Environment
50+
uses: actions/setup-go@v2
51+
with:
52+
go-version: '^1.20.0'
53+
54+
- name: Build Binaries
55+
run: |
56+
mkdir -p builds/compressed
57+
go install github.com/mitchellh/gox@latest
58+
cd saveshare
59+
gox --output "../../builds/saveshare-{{.OS}}-{{.Arch}}" -osarch 'darwin/amd64 linux/amd64 windows/amd64'
60+
cd ../../builds
61+
find . -maxdepth 1 -type f -execdir zip 'compressed/{}.zip' '{}' \;
62+
63+
- name: Upload Binaries
64+
run: |
65+
go install github.com/tcnksm/ghr@latest
66+
ghr -t ${{ secrets.GITHUB_TOKEN }} --delete Latest builds/compressed/

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
This is a Dockerized version of the [Satisfactory](https://store.steampowered.com/app/526870/Satisfactory/) dedicated server.
99

10+
If the server feels too buggy for you, you can try the [saveshare](saveshare/README.md) instead (which relies on client-hosting).
11+
1012
## Setup
1113

1214
Recent updates consume 4GB - 6GB RAM, but [the official wiki](https://satisfactory.wiki.gg/wiki/Dedicated_servers#Requirements) recommends allocating 12GB - 16GB RAM.

saveshare/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Satisfactory Save Sharing
2+
3+
**_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._**
4+
5+
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.
6+
7+
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.
8+
9+
## Setup
10+
11+
Download the release from the releases tab. When you initially run it, it'll ask for the following information:
12+
- Server address (IP and port, e.g. `localhost:15770`)
13+
- Server password (the SFTP password)
14+
- Session name (this must be EXACTLY as it is formatted within Satisfactory)
15+
16+
### Docker Compose
17+
18+
If you're using [Docker Compose](https://docs.docker.com/compose/):
19+
20+
```yaml
21+
version: '3'
22+
services:
23+
satisfactory-saveshare:
24+
container_name: satisfactory-saveshare
25+
image: atmoz/sftp:latest
26+
volumes:
27+
- /opt/saveshare:/home/saveshare/upload
28+
ports:
29+
- "15770:22"
30+
command: saveshare:PASSWORD_HERE:1001
31+
```
32+
33+
_Note: Do not change the username (`saveshare`) or the UID (`1001`). Only change the password._
34+
35+
### Known Issues
36+
You can't delete blueprints, unless you manually stop everyone from running the application and everyone deletes the blueprint locally (and server-side)

saveshare/config.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
type Config struct {
13+
gamePath string
14+
path string
15+
BlueprintPath string `yaml:"blueprintPath"`
16+
SavePath string `yaml:"savePath"`
17+
ServerAddress string `yaml:"serverAddress"`
18+
ServerPassword string `yaml:"serverPassword"`
19+
SessionName string `yaml:"sessionName"`
20+
}
21+
22+
func NewConfig(configDir string) (*Config, error) {
23+
cfg := Config{
24+
path: configDir + slash + "config.yml",
25+
}
26+
27+
// Check if the file exists
28+
if _, err := os.Stat(cfg.path); os.IsNotExist(err) {
29+
// If the file doesn't exist, create an empty file
30+
if _, err = os.Create(cfg.path); err != nil {
31+
return nil, fmt.Errorf("could not create config file: %w", err)
32+
}
33+
}
34+
35+
// Read the YAML file
36+
yamlData, err := os.ReadFile(cfg.path)
37+
if err != nil {
38+
return nil, fmt.Errorf("could not read config file: %w", err)
39+
}
40+
41+
if err = yaml.Unmarshal(yamlData, &cfg); err != nil {
42+
return nil, fmt.Errorf("could not unmarshal config file: %w", err)
43+
}
44+
45+
// populate the blueprint and save paths
46+
appDataPath, err := os.UserCacheDir()
47+
if err != nil {
48+
return nil, fmt.Errorf("could not get appdata path: %w", err)
49+
}
50+
51+
cfg.gamePath = appDataPath + slash + "FactoryGame" + slash + "Saved" + slash + "SaveGames" + slash
52+
53+
if cfg.SessionName != "" {
54+
cfg.BlueprintPath = cfg.gamePath + "blueprints" + slash + cfg.SessionName
55+
}
56+
57+
// check if the game path exists
58+
if _, err = os.Stat(cfg.gamePath); os.IsNotExist(err) {
59+
return nil, fmt.Errorf("game path does not exist: %w", err)
60+
}
61+
62+
// get the save path
63+
if err = filepath.Walk(cfg.gamePath, func(path string, info os.FileInfo, err error) error {
64+
if info == nil {
65+
return errors.New("path does not exist")
66+
}
67+
68+
if info.IsDir() {
69+
if cfg.SavePath == "" && path != cfg.gamePath {
70+
cfg.SavePath = path
71+
}
72+
return nil
73+
}
74+
75+
return nil
76+
}); err != nil {
77+
return nil, fmt.Errorf("could not walk over path: %w", err)
78+
}
79+
80+
if cfg.SavePath == "" {
81+
return nil, fmt.Errorf("could not find save path")
82+
}
83+
84+
return &cfg, nil
85+
}
86+
87+
func (c *Config) Save() error {
88+
yamlData, err := yaml.Marshal(c)
89+
if err != nil {
90+
return fmt.Errorf("could not marshal config file: %w", err)
91+
}
92+
93+
if err = os.WriteFile(c.path, yamlData, 0o644); err != nil {
94+
return fmt.Errorf("could not write config file: %w", err)
95+
}
96+
97+
return nil
98+
}

saveshare/docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: '3'
2+
services:
3+
satisfactory-saveshare:
4+
container_name: satisfactory-saveshare
5+
image: atmoz/sftp:latest
6+
volumes:
7+
- /opt/saveshare:/home/saveshare/upload
8+
ports:
9+
- "15770:22"
10+
command: saveshare:PASSWORD_HERE:1001

saveshare/file.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
8+
"github.com/pkg/sftp"
9+
)
10+
11+
func downloadFile(sftpClient *sftp.Client, remotePath, localPath string) error {
12+
remoteFile, err := sftpClient.Open(remotePath)
13+
if err != nil {
14+
return fmt.Errorf("error opening remote file: %v: %w", remotePath, err)
15+
}
16+
defer remoteFile.Close()
17+
18+
localFile, err := os.Create(localPath)
19+
if err != nil {
20+
return fmt.Errorf("error creating local file: %w", err)
21+
}
22+
defer localFile.Close()
23+
24+
if _, err = io.Copy(localFile, remoteFile); err != nil {
25+
return fmt.Errorf("error copying file: %w", err)
26+
}
27+
28+
fileInfo, err := remoteFile.Stat()
29+
if err != nil {
30+
return fmt.Errorf("error getting remote file info: %w", err)
31+
}
32+
33+
if err = os.Chtimes(localPath, fileInfo.ModTime(), fileInfo.ModTime()); err != nil {
34+
return fmt.Errorf("error setting local file mod time: %w", err)
35+
}
36+
37+
return nil
38+
}
39+
40+
func uploadFile(sftpClient *sftp.Client, localPath, remotePath string) error {
41+
localFile, err := os.Open(localPath)
42+
if err != nil {
43+
return fmt.Errorf("error opening local file: %w", err)
44+
}
45+
defer localFile.Close()
46+
47+
remoteFile, err := sftpClient.Create(remotePath)
48+
if err != nil {
49+
return fmt.Errorf("error creating remote file: %w", err)
50+
}
51+
defer remoteFile.Close()
52+
53+
if _, err = io.Copy(remoteFile, localFile); err != nil {
54+
return fmt.Errorf("error copying file: %w", err)
55+
}
56+
57+
fileInfo, err := localFile.Stat()
58+
if err != nil {
59+
return fmt.Errorf("error getting local file info: %w", err)
60+
}
61+
62+
if err = sftpClient.Chtimes(remotePath, fileInfo.ModTime(), fileInfo.ModTime()); err != nil {
63+
return fmt.Errorf("error setting remote file mod time: %w", err)
64+
}
65+
66+
return nil
67+
}

saveshare/go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module github.com/wolveix/satisfactory-server/saveshare
2+
3+
go 1.20
4+
5+
require (
6+
github.com/pkg/sftp v1.13.6
7+
github.com/rs/zerolog v1.30.0
8+
golang.org/x/crypto v0.13.0
9+
gopkg.in/yaml.v3 v3.0.1
10+
)
11+
12+
require (
13+
github.com/kr/fs v0.1.0 // indirect
14+
github.com/mattn/go-colorable v0.1.12 // indirect
15+
github.com/mattn/go-isatty v0.0.14 // indirect
16+
golang.org/x/sys v0.12.0 // indirect
17+
)

saveshare/go.sum

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
6+
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
7+
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
8+
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
9+
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
10+
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
11+
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
12+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
13+
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
14+
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
15+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
16+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
17+
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
18+
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
19+
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
20+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
21+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
22+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
23+
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
24+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
25+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
26+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
27+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
28+
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
29+
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
30+
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
31+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
32+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
33+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
34+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
35+
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
36+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
37+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
38+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
39+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
40+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
42+
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
44+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
45+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
46+
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
47+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
48+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
49+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
50+
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
51+
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
52+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
53+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
54+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
55+
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
56+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
57+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
58+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
59+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
60+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
61+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
62+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
63+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
64+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)