1 Commits

Author SHA1 Message Date
3ee6b1dce1 upgraded config creation 2025-06-09 21:38:39 -07:00
10 changed files with 86 additions and 196 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
.idea .idea
bin/ bin/
epochcli-* epochcli-*
epochcli
*.tar.gz *.tar.gz

View File

@ -1,16 +0,0 @@
class Epochcli < Formula
desc "Updater and Launcher for Epoch"
homepage "https://git.burkey.co/eburk/epochcli/src/branch/master"
license "ISC"
head "https://git.burkey.co/eburk/epochcli.git", branch: "master"
depends_on "go" => :build
def install
system "go", "build", *std_go_args(ldflags: "-s -w")
end
test do
assert_match "Epochcli Help:", shell_output("#{bin}/epochcli -h")
end
end

View File

@ -1,46 +1,27 @@
# epochcli # epochcli
CLI tool for updating and launching [Project Epoch](https://www.project-epoch.net/) on Windows, Linux and macOS. CLI tool for updating and launching [Project Epoch](https://www.project-epoch.net/) on Linux & macOS.
## Installing ## Setup Instructions
For Linux, a `wine` prefix with `dxvk` installed is sufficient, or you can use something like Lutris or faugus-launcher without the launcher functionality in `epochcli`
### Linux For macOS, the [Kegworks Wineskin port](https://github.com/Kegworks-App/Kegworks) works great (tutorial coming in the future...)
A `wine` prefix with `dxvk` installed is sufficient, or you can use something like Lutris or faugus-launcher and just use `epochcli` for updating.
Download and extract the latest binary from the [releases](https://git.burkey.co/eburk/epochcli/releases) page, build from source yourself, or use homebrew from the macOS instructions to install. 1. Install `epochcli` by either
1. Downloading the latest binary from the [releases](https://git.burkey.co/eburk/epochcli/releases) page
### macOS 2. If you have the `go` toolchain installed, you can run `go install git.burkey.co/eburk/epochcli` to install to your `$GOROOT`
3. Compile the source yourself
For macOS, I've found the best way to run Wow is in a Parallels Win 11 VM. Kegworks, Codeweavers, etc crash when the game starts up and I have not found a good solution so far. Any suggestions would be welcome, see my contact information below. I currently use a Parallels VM and run the Windows version of epochcli inside the VM as an updater and launcher. 2. Run `epochcli`. You will be taken through a setup process that configures the program and creates a config file at `$HOME/.config/epochcli/config.toml`
3. You can now use `epochcli` as just a standalone updater or also a launcher based on your configuration. You can always run `epochcli -c` to redo the configuration or edit the config file manually
You can easily install with homebrew or build from source yourself. I dont have time to setup codesigning right now so there are no binaries provided for macOS. For homebrew, do the following:
```shell
brew tap eburk/epochcli https://git.burkey.co/eburk/epochcli
brew install --HEAD epochcli
# To update
brew upgrade epochcli --fetch-HEAD
```
### Windows
Download and extract the latest binary from the [releases](https://git.burkey.co/eburk/epochcli/releases) page or build from source yourself, then copy `epochcli.exe` to a location of your choice. To make it easy, just use the same folder as your Wow game files.
## Setup
1. Run `epochcli`. You will be taken through a setup process that configures the program and creates a config file at `$HOME/.config/epochcli/config.toml`
2. You can now use `epochcli` as a standalone updater, but it can also act as a launcher based on your configuration. You can always run `epochcli -c` to redo the configuration, or edit the config file manually
## Usage ## Usage
``` ```
> ./epochcli -h > ./epochcli -h
-c Runs config configuration step. Overrides the config file -c Runs config configuration step. Overrides the config file
-f Forces epochcli to update files even if they match the current version
-h Print help -h Print help
-u Ignore EnableLauncher setting in config and only runs an update. Does nothing if EnableLauncher is false -u Ignore EnableLauncher setting in config and only runs an update. Does nothing if EnableLauncher is false
``` ```
## Issues ## Issues
If you have any issues, [email me](mailto:epochcli@burkey.co) or send a private message to `Battlehammer` on the Epoch discord If you have any issues, [email me](mailto:evan@burkey.co) or ping `Battlehammer` on the Epoch discord

View File

@ -1,13 +1,12 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/sqweek/dialog"
"os" "os"
"path"
"path/filepath" "path/filepath"
"runtime"
"strings"
) )
type Config struct { type Config struct {
@ -24,11 +23,10 @@ const (
var cfgPath string var cfgPath string
func setupConfig(rerun bool) (*Config, error) { func setupConfig(rerun bool) (*Config, error) {
home, err := os.UserHomeDir() home := os.Getenv("HOME")
if err != nil { if home == "" {
return nil, fmt.Errorf("unable to determine home directory: %v", err) return nil, fmt.Errorf("$HOME environment variable not set")
} }
cfgPath = filepath.Join(home, ".config", configDirName, configName)
newConfig := Config{ newConfig := Config{
WowDir: defaultWowPath, WowDir: defaultWowPath,
@ -36,56 +34,44 @@ func setupConfig(rerun bool) (*Config, error) {
EnableLauncher: false, EnableLauncher: false,
} }
cfgPath = filepath.Join(home, ".config", configDirName, configName)
_, statErr := os.Stat(cfgPath) _, statErr := os.Stat(cfgPath)
if rerun || os.IsNotExist(statErr) { if rerun || os.IsNotExist(statErr) {
fmt.Println("Enter the path to your Wow directory below:") fmt.Println("Press any key to open a file window and select your wow directory")
var s string var r rune
_, err = fmt.Scanln(&s) _, _ = fmt.Scanf("%c", &r)
var err error
newConfig.WowDir, err = dialog.Directory().Title("Select your wow directory").Browse()
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read input: %v", err) if errors.Is(err, dialog.ErrCancelled) {
return nil, fmt.Errorf("cancelled dialog box, exiting")
}
return nil, err
} }
newConfig.WowDir = strings.TrimSpace(s)
for { for {
fmt.Printf("Do you want to use epochcli to launch Wow? Select No if you plan on using a launcher tool like Lutris (y/n): ") fmt.Printf("Do you want to use epochcli to launch Wow? Select No if you plan on using a launcher tool like Lutris (y/n): ")
var s string
_, err = fmt.Scanf("%s", &s) _, err = fmt.Scanf("%s", &s)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read input: %v", err) return nil, err
} }
s = strings.TrimSpace(s)
if s == "y" || s == "Y" { if s == "y" || s == "Y" {
newConfig.EnableLauncher = true newConfig.EnableLauncher = true
if runtime.GOOS == "windows" {
newConfig.LaunchCmd = path.Join(newConfig.WowDir, "Project-Epoch.exe")
exePath, err := os.Executable()
if err != nil {
fmt.Println("unable to create desktop shortcut: ", err)
break
}
err = makeLink(exePath, path.Join(home, "Desktop", "Project-Epoch.lnk"))
if err != nil {
fmt.Println("unable to create desktop shortcut: ", err)
}
break
} else {
fmt.Println("Enter your launch command to start Wow below. If you would rather configure this later in the configuration file, just press Enter") fmt.Println("Enter your launch command to start Wow below. If you would rather configure this later in the configuration file, just press Enter")
fmt.Printf("> ") fmt.Printf("> ")
_, err = fmt.Scanf("%s", &s) _, err = fmt.Scanf("%s", &s)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read input: %v", err) return nil, err
} }
s = strings.TrimSpace(s)
if s != "" { if s != "" {
newConfig.LaunchCmd = s newConfig.LaunchCmd = s
} }
break break
} }
}
if s == "n" || s == "N" { if s == "n" || s == "N" {
break break
@ -94,26 +80,23 @@ func setupConfig(rerun bool) (*Config, error) {
fmt.Println("Please enter a valid value of either 'y' or 'n'") fmt.Println("Please enter a valid value of either 'y' or 'n'")
} }
err = os.MkdirAll(filepath.Join(home, ".config", configDirName), 0755) os.MkdirAll(filepath.Join(home, ".config", configDirName), 0755)
if err != nil {
return nil, fmt.Errorf("unable to create config directory: %v", err)
}
file, err := os.Create(cfgPath) file, err := os.Create(cfgPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create config file: %v", err) return nil, err
} }
defer file.Close() defer file.Close()
encoder := toml.NewEncoder(file) encoder := toml.NewEncoder(file)
if err = encoder.Encode(newConfig); err != nil { if err = encoder.Encode(newConfig); err != nil {
return nil, fmt.Errorf("unable to encode config file: %v", err) return nil, err
} }
fmt.Println("Created new config at ", cfgPath) fmt.Println("Created new config at ", cfgPath)
} }
_, err = toml.DecodeFile(cfgPath, &newConfig) _, err := toml.DecodeFile(cfgPath, &newConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,8 +0,0 @@
//go:build linux || darwin
package main
func makeLink(src, dst string) error {
// Noop on unix
return nil
}

View File

@ -1,36 +0,0 @@
package main
import (
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
"runtime"
)
func makeLink(src, dst string) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_SPEED_OVER_MEMORY)
if err != nil {
return err
}
oleShellObject, err := oleutil.CreateObject("WScript.Shell")
if err != nil {
return err
}
defer oleShellObject.Release()
wshell, err := oleShellObject.QueryInterface(ole.IID_IDispatch)
if err != nil {
return err
}
defer wshell.Release()
cs, err := oleutil.CallMethod(wshell, "CreateShortcut", dst)
if err != nil {
return err
}
idispatch := cs.ToIDispatch()
oleutil.PutProperty(idispatch, "TargetPath", src)
oleutil.CallMethod(idispatch, "Save")
return nil
}

4
go.mod
View File

@ -4,7 +4,7 @@ go 1.24.3
require ( require (
github.com/BurntSushi/toml v1.5.0 github.com/BurntSushi/toml v1.5.0
github.com/go-ole/go-ole v1.3.0 github.com/sqweek/dialog v0.0.0-20240226140203-065105509627
) )
require golang.org/x/sys v0.1.0 // indirect require github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect

8
go.sum
View File

@ -1,6 +1,6 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 h1:2JL2wmHXWIAxDofCK+AdkFi1KEg3dgkefCsm7isADzQ=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= github.com/sqweek/dialog v0.0.0-20240226140203-065105509627/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw=

46
main.go
View File

@ -25,18 +25,15 @@ func main() {
var ( var (
helpFlag bool helpFlag bool
updateOnlyFlag bool updateOnlyFlag bool
forceFlag bool
rerunConfig bool rerunConfig bool
) )
flag.BoolVar(&helpFlag, "h", false, "Print help") flag.BoolVar(&helpFlag, "h", false, "Print help")
flag.BoolVar(&forceFlag, "f", false, "Forces epochcli to update files even if they match the current version")
flag.BoolVar(&updateOnlyFlag, "u", false, "Ignore EnableLauncher setting in config and only runs an update. Does nothing if EnableLauncher is false") flag.BoolVar(&updateOnlyFlag, "u", false, "Ignore EnableLauncher setting in config and only runs an update. Does nothing if EnableLauncher is false")
flag.BoolVar(&rerunConfig, "c", false, "Runs config configuration step. Overrides the config file") flag.BoolVar(&rerunConfig, "c", false, "Runs config configuration step. Overrides the config file")
flag.Parse() flag.Parse()
if helpFlag { if helpFlag {
flag.CommandLine.SetOutput(os.Stdout) flag.CommandLine.SetOutput(os.Stdout)
fmt.Println("Epochcli Help:")
flag.PrintDefaults() flag.PrintDefaults()
os.Exit(0) os.Exit(0)
} }
@ -50,14 +47,14 @@ func main() {
log.Fatalf("WowDir in %s is still the default setting, exiting", cfgPath) log.Fatalf("WowDir in %s is still the default setting, exiting", cfgPath)
} }
stats, err := downloadUpdate(config, forceFlag) updated, current, err := downloadUpdate(config)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fmt.Printf("%d files updated\n", stats.updated) fmt.Printf("Updated %d files\n", updated)
if stats.current > 0 { if current > 0 {
fmt.Printf("%d files are already up to date\n", stats.current) fmt.Printf("%d files are already up to date\n", current)
} }
if updateOnlyFlag { if updateOnlyFlag {
@ -70,21 +67,17 @@ func main() {
} }
fmt.Println("Starting Epoch...") fmt.Println("Starting Epoch...")
if runtime.GOOS == "darwin" { switch runtime.GOOS {
case "darwin":
exec.Command("open", config.LaunchCmd).Run() exec.Command("open", config.LaunchCmd).Run()
} else { case "linux":
exec.Command(config.LaunchCmd).Run() exec.Command(config.LaunchCmd).Run()
} }
} }
} }
type DownloadStats struct { func downloadUpdate(config *Config) (int, int, error) {
updated int var updateCount, currentCount int
current int
}
func downloadUpdate(config *Config, force bool) (DownloadStats, error) {
var stats DownloadStats
manifest, err := getManifest() manifest, err := getManifest()
if err != nil { if err != nil {
@ -101,46 +94,43 @@ func downloadUpdate(config *Config, force bool) (DownloadStats, error) {
os.MkdirAll(localDir, 0755) os.MkdirAll(localDir, 0755)
} }
if !force {
if _, err = os.Stat(localPath); err == nil { if _, err = os.Stat(localPath); err == nil {
data, err := os.ReadFile(localPath) data, err := os.ReadFile(localPath)
if err != nil { if err != nil {
return stats, err return updateCount, currentCount, err
} }
hashBytes := md5.Sum(data) hashBytes := md5.Sum(data)
hash := hex.EncodeToString(hashBytes[:]) hash := hex.EncodeToString(hashBytes[:])
if hash == file.Hash { if hash == file.Hash {
fmt.Printf("File %s is up to date\n", localPath) currentCount += 1
stats.current += 1
continue continue
} }
} }
}
fmt.Printf(" %s...\n", localPath) fmt.Printf("Updating %s...\n", file.Path)
outFile, err := os.Create(localPath) outFile, err := os.Create(localPath)
if err != nil { if err != nil {
return stats, err return updateCount, currentCount, err
} }
defer outFile.Close() defer outFile.Close()
resp, err := http.Get(file.URL) resp, err := http.Get(file.URL)
if err != nil { if err != nil {
return stats, err return updateCount, currentCount, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return stats, fmt.Errorf("failed to download update from %s, status code: %d", file.URL, resp.StatusCode) return updateCount, currentCount, fmt.Errorf("failed to download update from %s, status code: %d", file.URL, resp.StatusCode)
} }
_, err = io.Copy(outFile, resp.Body) _, err = io.Copy(outFile, resp.Body)
if err != nil { if err != nil {
return stats, err return updateCount, currentCount, err
} }
stats.updated += 1 updateCount += 1
} }
return stats, nil return updateCount, currentCount, nil
} }

View File

@ -1,22 +1,19 @@
#!/usr/bin/env bash #!/usr/bin/env sh
set -e set -e
rm -f ./*.tar.gz rm -f ./*.tar.gz
rm -f ./*.zip rm -rf bin
GOOS=linux GOARCH=amd64 go build -o epochcli mkdir bin
tar czvf epochcli-linux-amd64.tar.gz epochcli
rm ./epochcli
GOOS=linux GOARCH=arm64 go build -o epochcli GOOS=linux GOARCH=amd64 go build -o bin/epochcli-linux-amd64
tar czvf epochcli-linux-arm64.tar.gz epochcli tar czvf epochcli-linux-amd64.tar.gz bin/epochcli-linux-amd64
rm ./epochcli
GOOS=windows GOARCH=amd64 go build -o epochcli.exe GOOS=darwin GOARCH=amd64 go build -o bin/epochcli-darwin-amd64
zip -j epochcli-windows-amd64.zip epochcli.exe tar czvf epochcli-darwin-amd64.tar.gz bin/epochcli-darwin-amd64
rm ./epochcli.exe
GOOS=windows GOARCH=arm64 go build -o epochcli.exe GOOS=darwin GOARCH=arm64 go build -o bin/epochcli-darwin-arm64
zip -j epochcli-windows-arm64.zip epochcli.exe tar czvf epochcli-darwin-arm64.tar.gz bin/epochcli-darwin-arm64
rm ./epochcli.exe
rm -rf bin