Skip to content

Commit

Permalink
Release SaveShare
Browse files Browse the repository at this point in the history
  • Loading branch information
wolveix committed Dec 15, 2023
1 parent bc5c0cf commit 27efbfd
Show file tree
Hide file tree
Showing 10 changed files with 588 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ jobs:
with:
push: true
tags: |
ghcr.io/${{ github.repository }}/${{ github.event.repository.name }}:dev
ghcr.io/${{ github.repository }}:dev
${{ steps.meta.outputs.tags }}
23 changes: 21 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
# list of Docker images to use as base name for tags
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}/${{ github.event.repository.name }}
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
tags: |
type=semver,pattern=v{{version}}
Expand All @@ -44,4 +44,23 @@ jobs:
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
tags: ${{ steps.meta.outputs.tags }}

- name: Setup Go Environment
uses: actions/setup-go@v2
with:
go-version: '^1.20.0'

- name: Build Binaries
run: |
mkdir -p builds/compressed
go install github.com/mitchellh/gox@latest
cd saveshare
gox --output "../../builds/saveshare-{{.OS}}-{{.Arch}}" -osarch 'darwin/amd64 linux/amd64 windows/amd64'
cd ../../builds
find . -maxdepth 1 -type f -execdir zip 'compressed/{}.zip' '{}' \;
- name: Upload Binaries
run: |
go install github.com/tcnksm/ghr@latest
ghr -t ${{ secrets.GITHUB_TOKEN }} --delete Latest builds/compressed/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

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

If the server feels too buggy for you, you can try the [saveshare](saveshare/README.md) instead (which relies on client-hosting).

## Setup

Recent updates consume 4GB - 6GB RAM, but [the official wiki](https://satisfactory.wiki.gg/wiki/Dedicated_servers#Requirements) recommends allocating 12GB - 16GB RAM.
Expand Down
36 changes: 36 additions & 0 deletions saveshare/README.md
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)
98 changes: 98 additions & 0 deletions saveshare/config.go
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
}
10 changes: 10 additions & 0 deletions saveshare/docker-compose.yml
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
67 changes: 67 additions & 0 deletions saveshare/file.go
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
}
17 changes: 17 additions & 0 deletions saveshare/go.mod
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
)
64 changes: 64 additions & 0 deletions saveshare/go.sum
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=
Loading

0 comments on commit 27efbfd

Please sign in to comment.