125 Commits
master ... dev

Author SHA1 Message Date
0f09d23e67 Setup NSIS installer 2025-05-16 13:33:43 -06:00
b858d50746 replaced list with buttons 2025-05-07 07:23:00 -06:00
4ecdd3be6a upload app.svelte 2025-05-06 22:14:23 -06:00
2825b461af working on modpack ui 2025-05-06 22:13:34 -06:00
0f14bc2de7 Admin Page Login 2025-05-06 19:14:16 -06:00
008761e94a starting work on admin backend 2025-05-05 13:36:29 -06:00
2be1341404 maybe fixed auth issue? 2025-05-04 13:10:10 -06:00
3df2030f1e version bump 2025-05-03 19:32:58 -06:00
9a7a317a11 Pointed at gitea.piwalker.net 2025-05-03 19:29:36 -06:00
1d7df8854f New Instance Page Layout 2025-03-14 14:44:38 +11:00
2b21faf626 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2025-03-13 20:21:22 -06:00
1acf7db41d updated gitignore 2025-03-13 20:20:41 -06:00
e45d32a4ad wording change 2025-03-12 13:01:06 -06:00
11bda753c6 version 0.0.4 2025-03-11 15:57:54 -06:00
5a4307dc94 added login prompt parameter to oauth login 2025-03-11 15:34:23 -06:00
231b789545 Finished oauth2 2025-03-11 10:20:43 -06:00
a4316fa8de Working on OAuth2 authentication. 2025-03-11 08:28:29 -06:00
ce24c0a55d Created delete instance button, and made minor ui modifications 2025-03-10 11:38:27 -06:00
3b6aa14082 updated gitignore 2024-11-30 08:32:11 -07:00
=
ee19529a42 version 0.0.3 2024-11-26 21:42:06 -07:00
ca4713eb51 Weird build changes 2024-11-27 15:41:43 +11:00
=
9d61ea470a implemented open instance folder button 2024-11-26 21:37:26 -07:00
=
fab46be020 added test link 2024-11-26 21:34:38 -07:00
e8c24ee540 Instances UI 2024-11-27 15:34:12 +11:00
=
86cc464f45 version 0.0.2 2024-11-26 21:02:56 -07:00
ab6ad50983 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-27 14:56:43 +11:00
ec0fdb87c6 Theme Buttons 2024-11-27 14:56:39 +11:00
=
035d59b0f6 Implemented auto updating 2024-11-26 20:54:30 -07:00
a412cbf9d6 Theming works 2024-11-27 14:20:21 +11:00
cf31acdbc0 Settings page baybee 2024-11-27 13:55:43 +11:00
45efbffe7c Fixed nav hover 2024-11-27 13:41:30 +11:00
=
29c2351b2f use unix path for https requests 2024-11-26 16:49:32 -07:00
=
c34652bc4a replace path separators again 2024-11-26 16:45:08 -07:00
=
d5f8881d7e more path issues 2024-11-26 16:29:58 -07:00
=
0c111235a6 all unix baby! 2024-11-26 16:23:54 -07:00
=
377e42a24c linux separator? 2024-11-26 16:21:26 -07:00
=
eef50d5f2c more path debugging 2024-11-26 16:19:46 -07:00
=
3389119d03 fixed path madness 2024-11-26 16:16:13 -07:00
=
cf24d351e3 more debug code 2024-11-26 16:14:02 -07:00
=
49147a4edd debugging lib paths 2024-11-26 16:12:17 -07:00
=
20b8433d34 path separator weridness 2024-11-26 16:09:54 -07:00
=
6b07b95146 debugging maven paths 2024-11-26 16:03:28 -07:00
=
cddb8bb478 fix path splitting issue 2024-11-26 16:00:05 -07:00
40311e1b12 Fix weird prompt to commit 2024-11-27 09:58:20 +11:00
=
aed69860de remove auth file before re-writting 2024-11-26 15:52:29 -07:00
=
dadf5f641b https request fix path separator 2024-11-26 15:49:44 -07:00
=
4a35078f90 more error checking on modpack downloads 2024-11-26 15:37:35 -07:00
=
e7f8de116e Added error checking 2024-11-26 15:31:01 -07:00
=
22c18915f5 Changed url to point to new server 2024-11-26 12:37:30 -07:00
=
69791e5188 open browser for auth 2024-11-26 11:50:33 -07:00
=
d4f9711699 some random ui polish changes 2024-11-26 10:22:37 -07:00
=
ad736787e9 Changed default RAM limit 2024-11-25 19:21:20 -07:00
=
8208434b8f fixed overrides extraction 2024-11-25 14:55:04 -07:00
=
2b372423eb Finalized Modpack installation. 2024-11-25 14:28:32 -07:00
=
f905d617a8 Able to install correct version of minecraft + loader from mrpack 2024-11-25 13:23:06 -07:00
=
ad84711646 Working on modpack installation page 2024-11-25 12:36:29 -07:00
24f32cdba5 Fixed navbar flicker on windows 2024-11-05 11:56:10 +11:00
3e667f004d package json 2024-11-05 11:11:40 +11:00
f3e6f5d925 Fixed icons growing on windows 2024-11-05 11:09:46 +11:00
7e5b9596a4 global theming works 2024-11-04 13:23:47 +11:00
d15158f79c Changed color handling, in testing right now 2024-11-03 14:12:07 +11:00
d9dbd2d29b Replacing old ui, list population needs moved to app 2024-11-02 19:06:56 +11:00
3fe2b3df6d Moving page elements out of the way when the navbar opens 2024-11-02 18:32:24 +11:00
6f6a00cb45 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-02 12:14:08 +11:00
0923913801 Trying to make pages adjust dynamically for navbar 2024-11-02 12:14:04 +11:00
b9d8b763a7 working on forge integration 2024-11-01 18:08:29 -06:00
4946b7e595 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-01 08:05:14 -06:00
620636fb36 fixed reauth 2024-11-01 08:05:12 -06:00
ca9e41e99e Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-02 01:04:56 +11:00
129b1d1a19 Added launch text to launch button 2024-11-02 01:04:52 +11:00
59b836b40a change import extension 2024-11-01 07:51:38 -06:00
af6ff50cfd Instance switching in good ui 2024-11-02 00:44:39 +11:00
bd787bdfa2 More ui work 2024-11-02 00:12:40 +11:00
2d8ce26af9 Cool select UI 2024-11-01 23:41:52 +11:00
27c97010ae Made instances list global 2024-11-01 05:55:41 -06:00
9d23d503e2 array stuff in testpage 2024-11-01 22:43:18 +11:00
479ee2e6e0 removed logo because it was annoying to work around 2024-11-01 21:55:39 +11:00
7e290472fb Added a test page for constructing new ui 2024-11-01 21:50:35 +11:00
e5cf9f532e Pull forge versions 2024-10-31 21:20:20 -06:00
a614f71aa1 quilt implemented 2024-10-31 18:57:54 -06:00
46bfc92370 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-10-31 18:21:11 -06:00
f6b68b0b43 working on quilt 2024-10-31 18:21:09 -06:00
6709726709 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-01 11:20:10 +11:00
5745670c0a navbar controls things 2024-11-01 11:20:06 +11:00
cce23ec175 fixed work directory 2024-10-31 17:50:45 -06:00
d7cfdaf6f2 merge 2024-10-31 17:48:17 -06:00
bf4d4ac583 fixed fabric 2024-10-31 17:47:50 -06:00
525af0db42 background color 2024-11-01 10:38:55 +11:00
c1bab40aab Minor layout change 2024-11-01 10:03:42 +11:00
7e728ddff8 working on fabric issue 2024-10-31 16:46:12 -06:00
0f265ced92 Replaced stand-in items in navbar 2024-11-01 09:44:41 +11:00
859bf812cd fabric installing, sorta... 2024-10-31 16:23:38 -06:00
d0384b1778 merge 2024-10-31 14:57:12 -06:00
3951af01c9 fabric version selection 2024-10-31 14:55:58 -06:00
6179e669df logo color 2024-11-01 01:20:11 +11:00
a96dfa032d fixed text wrapping 2024-11-01 01:09:38 +11:00
58a7a472fc Change navbar text 2024-11-01 01:05:55 +11:00
189e3e438f implemented navbar 2024-11-01 01:00:46 +11:00
ca21acf6b1 Started Navbar 2024-10-31 13:38:56 +11:00
25426e749b updated gitignor 2024-10-30 19:35:29 -06:00
e36eabe5df merge 2024-10-30 19:34:21 -06:00
e71593bf3a updated gitignore 2024-10-30 19:33:50 -06:00
10d373fd4b Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-10-31 12:32:31 +11:00
dec56ffcb2 added Navbar svelte 2024-10-31 12:29:26 +11:00
774b623f1d random tweaks 2024-10-30 19:11:24 -06:00
7c169ce4a7 finished auth 2024-10-30 17:16:10 -06:00
806f6e2dbf initial microsoft auth 2024-10-30 16:41:03 -06:00
3720f1e524 added logging config 2024-10-30 15:58:05 -06:00
2bbb4b8cf0 installation and launching of vanilla minecraft working 2024-10-30 15:39:11 -06:00
ab0bfebe87 Started work on downloading minecraft 2024-10-29 21:48:59 -06:00
98e03a3e60 added basic slide transition 2024-10-26 23:39:21 -06:00
4e6ea6f22b fixed windows issues 2024-10-26 22:33:33 -06:00
99a5934575 working on windows version 2024-10-26 19:27:39 -06:00
b6af1549a3 fixed application not closing 2024-10-26 19:10:26 -06:00
bce2b04d3e almost have launching working 2024-10-26 18:14:12 -06:00
4bcc2703d6 Working on division between modpack and instance 2024-10-26 14:00:53 -06:00
db9ba30442 Fixed output name of modpack files 2024-10-25 22:58:52 -06:00
3c8fb9a89f added basic support for downloading (not installing) modpack files 2024-10-25 22:56:22 -06:00
29bf715025 removed some frontend testing code 2024-10-25 21:43:19 -06:00
8efd3a5631 Added support for installing java versions 2024-10-25 20:46:06 -06:00
bca9bec6d6 install prism, and display status in ui 2024-10-24 19:48:19 -06:00
7ad769ff96 Started https download code 2024-10-24 17:05:47 -06:00
0a6b37ccab fixed formatting 2024-10-24 15:49:14 -06:00
3b89a26265 got svelte working 2024-10-24 15:46:17 -06:00
c646c36a06 built wails project 2024-10-24 13:31:01 -06:00
142 changed files with 7442 additions and 0 deletions

8
.gitignore vendored
View File

@ -3,6 +3,8 @@
# will have compiled files and executables
debug/
target/
logs/
**/logs/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
@ -10,4 +12,10 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
**/*.log
FCLauncher/build/bin
FCLauncher/node_modules
FCLauncher/frontend/dist
FCLauncher/frontend/wailsjs/go/

View File

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 943 B

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 995 B

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

4
fclauncher/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
build/bin
node_modules
frontend/dist
frontend/wailsjs/go/

34
fclauncher/2 Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"context"
"os"
"path/filepath"
)
type Instance struct {
InstanceName string
ModpackId string
ModpackVersion string
}
type InstanceManager struct {
instances []Instance
PrismLauncher Prism
ctx context.Context
}
func (i *InstanceManager)SearchInstances() {
i.instances = []Instance{}
dir := i.PrismLauncher.GetInstanceDir()
subdirs, _ := os.ReadDir(dir)
for _, d := range subdirs {
if !d.IsDir() {
continue
}
if _, err := os.Stat(filepath.Join(dir, d.Name(), "instance.json")); err != nil {
continue
}
f, _ = os.OpenFile()
}
}

44
fclauncher/Admin.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"fmt"
"golang.org/x/crypto/ssh"
)
type Admin struct {
conf *ssh.ClientConfig
}
func sftpConnect(conf *ssh.ClientConfig) (ssh.Conn, error) {
return ssh.Dial("tcp", "gitea-svr.piwalker.net:22", conf)
}
func sftpAuth(username string, password string) (*ssh.ClientConfig, error) {
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte("gitea-svr.piwalker.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILkyh7MDvubWw4OzTFbvsUz7gOmOzBq77i5Q86STKqja"));
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.FixedHostKey(key),
}
conn, err := sftpConnect(config)
if err != nil {
return nil, err
}
conn.Close()
return config, nil
}
func (a *Admin) AdminAuth(username string, password string) bool {
conf, err := sftpAuth(username, password)
if err != nil {
fmt.Println("SFTP error: ", err)
a.conf = nil
return false
}
a.conf = conf
return true
}

View File

@ -0,0 +1,624 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/zhyee/zipstream"
)
type MrData struct {
FormatVersion int
Game string
VersionId string
Name string
Summary string
Files []MrFile
Dependencies map[string]string
}
type MrFile struct {
Path string
Hashes map[string]string
Env map[string]string
Downloads []string
FileSize int
}
type Instance struct {
InstanceName string
ModpackId string
ModpackVersion string
MinecraftVersion string
ForgeVersion string
NeoForgeVersion string
FabricVersion string
QuiltVersion string
JavaVersion int
Libraries []string
MainClass string
}
type InstanceManager struct {
instances []Instance
app *App
}
type mmcpack struct {
Components []component
}
type component struct {
Uid string
Version string
}
func (i *InstanceManager) SearchInstances() {
i.instances = []Instance{}
dir, _ := os.UserConfigDir()
dir = filepath.Join(dir, "FCLauncher", "instances")
if _, err := os.Stat(dir); err != nil {
return
}
subdirs, _ := os.ReadDir(dir)
for _, d := range subdirs {
if !d.IsDir() {
continue
}
if _, err := os.Stat(filepath.Join(dir, d.Name(), "instance.json")); err != nil {
continue
}
f, _ := os.OpenFile(filepath.Join(dir, d.Name(), "instance.json"), os.O_RDONLY, 0755)
defer f.Close()
buff := new(bytes.Buffer)
io.Copy(buff, f)
instance := Instance{}
json.Unmarshal(buff.Bytes(), &instance)
i.instances = append(i.instances, instance)
}
}
func (i *InstanceManager) checkJavaVersion(instance Instance) {
infoPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "mmc-pack.json")
f, _ := os.OpenFile(infoPath, os.O_RDONLY, 0755)
defer f.Close()
dataStr, _ := io.ReadAll(f)
var data mmcpack
json.Unmarshal(dataStr, &data)
mc_version := "0.0"
for _, comp := range data.Components {
if comp.Uid == "net.minecraft" {
mc_version = comp.Version
break
}
}
fmt.Printf("MC Version: %s", mc_version)
tokensStr := strings.Split(mc_version, ".")
tokens := []int{0, 0, 0}
tokens[0], _ = strconv.Atoi(tokensStr[0])
tokens[1], _ = strconv.Atoi(tokensStr[1])
if len(tokensStr) > 2 {
tokens[2], _ = strconv.Atoi(tokensStr[2])
}
javaVer := 8
if tokens[1] == 17 {
javaVer = 17
} else if tokens[1] == 18 || tokens[1] == 19 {
javaVer = 17
} else if tokens[1] > 19 {
if tokens[1] == 20 && tokens[2] < 5 {
javaVer = 17
} else {
javaVer = 21
}
}
fmt.Printf("Req Java Version: %d", javaVer)
if !i.app.Java.CheckJavaVer(javaVer) {
i.app.Java.InstallJavaVer(javaVer)
}
confPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.cfg")
f, _ = os.OpenFile(confPath, os.O_RDONLY, 0755)
defer f.Close()
buff := new(bytes.Buffer)
io.Copy(buff, f)
sc := bufio.NewScanner(buff)
f, _ = os.OpenFile(confPath, os.O_CREATE|os.O_RDWR, 0755)
plat := "lin"
exe := "java"
if runtime.GOOS == "windows" {
plat = "win"
exe = "Java.exe"
}
confDir, _ := os.UserConfigDir()
found := false
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "JavaPath=") {
line = fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe)
found = true
}
f.WriteString(line + "\n")
}
if !found {
line := fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe)
f.WriteString(line + "\n")
f.WriteString("OverrideJavaLocation=true\nOverrideJava=true\n")
}
f.Close()
}
func (i *InstanceManager) InstallModpack(modpack Modpack, instanceName string) {
i.app.Status(fmt.Sprintf("Installing %s", modpack.Name))
version := modpack.Versions[len(modpack.Versions)-1]
dname, _ := os.MkdirTemp("", "fclauncher-*")
f, _ := os.OpenFile(filepath.Join(dname, instanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
HttpDownload(modpack.Id+"/"+version.File, f, i.app.Ctx)
i.app.PrismLauncher.ImportModpack(f.Name())
instance := Instance{InstanceName: instanceName, ModpackVersion: version.Version, ModpackId: modpack.Id}
i.instances = append(i.instances, instance)
f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
data, _ := json.Marshal(instance)
f.Write(data)
i.checkJavaVersion(instance)
i.SearchInstances()
}
func (i *InstanceManager) InstallVanilla(version string, instanceName string) {
dir, _ := os.UserConfigDir()
err := DownloadAssets(version, filepath.Join(dir, "FCLauncher", "assets"), *i.app)
if err != nil {
fmt.Printf("Unable to download assets: %s\n", err)
} else {
fmt.Printf("Assets Downloaded")
}
err = DownloadLibraries(version, filepath.Join(dir, "FCLauncher", "lib"), *i.app)
if err != nil {
fmt.Printf("Unable to download libs: %s\n", err)
} else {
fmt.Printf("Libs Downloaded")
}
InstallNatives(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft", "natives"))
DownloadLoggingConfig(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft"))
err = DownloadExecutable(version, filepath.Join(dir, "FCLauncher", "bin"), *i.app)
if err != nil {
fmt.Printf("Unable to download binaries: %s\n", err)
} else {
fmt.Printf("Binaries Downloaded")
}
metadata, err := GetVersionMetadata(version)
if err != nil {
fmt.Printf("unable to pull metadata: %s\n", err)
}
err = os.MkdirAll(filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft"), 0755)
if err != nil {
fmt.Printf("unable to create directory: %s\n", err)
}
instance := Instance{InstanceName: instanceName, MinecraftVersion: version, JavaVersion: metadata.JavaVersion.MajorVersion, MainClass: metadata.MainClass}
for _, lib := range metadata.Libraries {
instance.Libraries = append(instance.Libraries, lib.Downloads.Artifact.Path)
}
data, err := json.Marshal(instance)
if err != nil {
fmt.Printf("unable to marshal json data: %s\n", err)
}
f, err := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
if err != nil {
fmt.Printf("unable to open file: %s\n", err)
}
defer f.Close()
_, err = f.Write(data)
if err != nil {
fmt.Printf("unable to write data: %s\n", err)
}
i.instances = append(i.instances, instance)
if !i.app.Java.CheckJavaVer(instance.JavaVersion) {
i.app.Status(fmt.Sprintf("Installing Java Version %d", instance.JavaVersion))
i.app.Java.InstallJavaVer(instance.JavaVersion)
}
i.SearchInstances()
}
func (i *InstanceManager) GetInstances() []string {
names := []string{}
for _, inst := range i.instances {
names = append(names, inst.InstanceName)
}
return names
}
func (i *InstanceManager) CheckUpdate(instance Instance) {
return
i.app.Status("Checking for Updates")
i.app.Modpacks.QuerryModpacks()
pack := i.app.Modpacks.GetModpack(instance.ModpackId)
if pack.Versions[len(pack.Versions)-1].Version == instance.ModpackVersion {
return
}
i.app.Status(fmt.Sprintf("Updating %s", instance.InstanceName))
version := pack.Versions[len(pack.Versions)-1]
dname, _ := os.MkdirTemp("", "fclauncher-*")
f, _ := os.OpenFile(filepath.Join(dname, instance.InstanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
HttpDownload(pack.Id+"/"+version.File, f, i.app.Ctx)
i.app.PrismLauncher.ImportModpack(f.Name())
instance.ModpackVersion = version.Version
f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
data, _ := json.Marshal(instance)
f.Write(data)
i.checkJavaVersion(instance)
i.SearchInstances()
}
func (i *InstanceManager) GetInstance(instance string) (Instance, error) {
instanceObject := Instance{}
found := false
for _, inst := range i.instances {
if inst.InstanceName == instance {
instanceObject = inst
found = true
break
}
}
if !found {
return Instance{}, fmt.Errorf("unable to find instance %s\n", instance)
}
return instanceObject, nil
}
func (i *InstanceManager) LaunchInstance(instance string) {
i.app.Status(fmt.Sprintf("Launching %s", instance))
dir, err := os.UserConfigDir()
if err != nil {
fmt.Printf("unable to get config directory\n")
}
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Unable to find instance\n")
}
execName := "java"
suffix := "lin"
if runtime.GOOS == "windows" {
execName = "Javaw.exe"
suffix = "win"
}
dir = filepath.Join(dir, "FCLauncher")
auth, err := MicrosoftAuth(i.app.Auth)
if err != nil {
fmt.Printf("unable to authenticate: %s\n", err)
return
}
args, err := GetOnlineLaunchArgs(instanceObject.MinecraftVersion, instanceObject, filepath.Join(dir, "lib"), filepath.Join(dir, "bin"), filepath.Join(dir, "assets"), filepath.Join(dir, "instances", instance, "minecraft"), auth)
if err != nil {
fmt.Printf("unable to get launch args: %s\n", err)
}
if instanceObject.ForgeVersion != "" {
args = append(args, "--launchTarget")
args = append(args, "forge_client")
}
fmt.Printf("Args: %+v", args)
child := exec.Command(filepath.Join(dir, "java", fmt.Sprintf("java-%d-%s", instanceObject.JavaVersion, suffix), "bin", execName), args...)
child.Dir = filepath.Join(dir, "instances", instance, "minecraft")
wruntime.WindowHide(i.app.Ctx)
data, err := child.CombinedOutput()
wruntime.WindowShow(i.app.Ctx)
fmt.Printf("Command Output: %s\n", data)
}
func (i *InstanceManager) InstallFabric(instance string, fabricVersion string) {
i.app.Status("Installing Fabric")
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
metadata, err := GetFabricMetadata(instanceObject.MinecraftVersion, fabricVersion)
if err != nil {
fmt.Printf("unable to get version metadata\n")
}
client:
for _, lib := range metadata.LauncherMeta.Libraries.Client {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue client
}
}
}
common:
for _, lib := range metadata.LauncherMeta.Libraries.Common {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
fmt.Printf("Inspecing path %s with %d tokens\n", path, len(tokens))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue common
}
}
}
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven)))
instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"]
instanceObject.FabricVersion = fabricVersion
dir, _ := os.UserConfigDir()
InstallFabricLibs(instanceObject.MinecraftVersion, fabricVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) InstallQuilt(instance string, quiltVersion string) {
i.app.Status("Installing Quilt")
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
metadata, err := GetQuiltMetadata(instanceObject.MinecraftVersion, quiltVersion)
if err != nil {
fmt.Printf("unable to get version metadata\n")
}
client:
for _, lib := range metadata.LauncherMeta.Libraries.Client {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue client
}
}
}
common:
for _, lib := range metadata.LauncherMeta.Libraries.Common {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue common
}
}
}
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Hashed.Maven), ProcessMavenFilename(metadata.Hashed.Maven)))
instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"]
instanceObject.QuiltVersion = quiltVersion
dir, _ := os.UserConfigDir()
InstallQuiltLibs(instanceObject.MinecraftVersion, quiltVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) InstallForge(instance string, forgeVersion string) {
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Unable to find instance: %s\n", err)
}
installData, err := GetForgeInstallData(instanceObject.MinecraftVersion, forgeVersion)
if err != nil {
fmt.Printf("Unable to get install data: %s\n", err)
}
dir, _ := os.UserConfigDir()
InstallForgeLibs(instanceObject.MinecraftVersion, forgeVersion, filepath.Join(dir, "FCLauncher", "lib"))
instanceObject.ForgeVersion = forgeVersion
outer:
for _, lib := range installData.Libraries {
tokens := strings.Split(lib.Downloads.Artifact.Path, string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, lib.Downloads.Artifact.Path)
for ind, path := range instanceObject.Libraries {
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue outer
}
}
}
instanceObject.MainClass = installData.MainClass
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) ImportModpack(modpack Modpack, name string) {
i.app.Status(fmt.Sprintf("Downloading %s", modpack.Name))
buff := new(bytes.Buffer)
err := HttpDownload(filepath.Join(modpack.Id, modpack.Versions[len(modpack.Versions)-1].File), buff, i.app.Ctx)
if err != nil {
fmt.Printf("Unable to download modpack file: %s\n", err)
return
}
i.ImportMrpack(buff, name)
}
func (i *InstanceManager) ImportMrpack(data io.Reader, name string) {
dir, _ := os.UserConfigDir()
InstancePath := filepath.Join(dir, "FCLauncher", "instances", name, "minecraft")
zr := zipstream.NewReader(data)
mrdata := MrData{}
i.app.Status("Unpacking modpack File")
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
fmt.Printf("Error unpacking modpack file: %s\n", err)
break
}
if entry.Name == "modrinth.index.json" {
i.app.Status("Loading metadata")
file, _ := entry.Open()
data, _ := io.ReadAll(file)
json.Unmarshal(data, &mrdata)
} else {
i.app.Status(fmt.Sprintf("Unpacking %s", entry.Name))
prefix := strings.Split(entry.Name, "/")[0]
if prefix == "overrides" || prefix == "client-overrides" {
path := strings.SplitN(entry.Name, "/", 2)[1]
if entry.IsDir() {
fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, path))
if _, err := os.Stat(filepath.Join(InstancePath, path)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, path), 0755)
}
} else {
zf, _ := entry.Open()
defer zf.Close()
fileDir := ""
tokens := strings.Split(path, "/")
for ind, token := range tokens {
if ind != len(tokens)-1 {
fileDir = filepath.Join(fileDir, token)
}
}
fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, fileDir))
if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755)
}
file, _ := os.OpenFile(filepath.Join(InstancePath, path), os.O_CREATE|os.O_RDWR, 0755)
defer file.Close()
io.Copy(file, zf)
}
}
}
}
i.InstallVanilla(mrdata.Dependencies["minecraft"], name)
if mrdata.Dependencies["forge"] != "" {
fmt.Printf("Forge not implemented!")
//implement forge
} else if mrdata.Dependencies["neoforge"] != "" {
fmt.Printf("Neoforge not implemented!")
//implement neoforge
} else if mrdata.Dependencies["fabric-loader"] != "" {
i.InstallFabric(name, mrdata.Dependencies["fabric-loader"])
} else if mrdata.Dependencies["quilt-loader"] != "" {
i.InstallQuilt(name, mrdata.Dependencies["quilt-loader"])
}
i.app.Status("Downloading Mods")
for _, f := range mrdata.Files {
fmt.Printf("Downloading %s\n", f.Path)
i.app.Status(fmt.Sprintf("Downloading %s", f.Path))
resp, err := http.Get(f.Downloads[0])
if err != nil {
fmt.Printf("Unable to download file %s\n", err)
continue
}
defer resp.Body.Close()
buff := new(bytes.Buffer)
downloaded := 0
for {
count, err := io.CopyN(buff, resp.Body, BlockSize)
if err == io.EOF {
downloaded += int(count)
break
}
if err != nil {
fmt.Printf("Error Downloading libs: %e\n", err)
return
}
downloaded += int(count)
wruntime.EventsEmit(i.app.Ctx, "download", downloaded, f.FileSize)
}
fileDir := ""
tokens := strings.Split(f.Path, "/")
for ind, token := range tokens {
if ind != len(tokens)-1 {
fileDir = filepath.Join(fileDir, token)
}
}
if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755)
}
file, _ := os.OpenFile(filepath.Join(InstancePath, f.Path), os.O_CREATE|os.O_RDWR, 0755)
defer file.Close()
io.Copy(file, buff)
wruntime.EventsEmit(i.app.Ctx, "download_complete")
}
}
func (i *InstanceManager) OpenInstanceFolder(instance string) {
_, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
dir, _ := os.UserConfigDir()
openbrowser(filepath.Join(dir, "FCLauncher", "instances", instance, "minecraft"))
}
func (i *InstanceManager) DeleteInstance(instance string) {
_, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
return
}
dir, _ := os.UserConfigDir()
os.RemoveAll(filepath.Join(dir, "FCLauncher", "instances", instance))
i.SearchInstances()
}

120
fclauncher/Java.go Normal file
View File

@ -0,0 +1,120 @@
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/zhyee/zipstream"
)
type JavaManager struct {
app *App
}
func (JavaManager)CheckJavaVer(version int) bool{
suffix := "lin"
if runtime.GOOS == "windows" {
suffix = "win"
}
path, _ := os.UserConfigDir()
path = filepath.Join(path, "FCLauncher", "java", fmt.Sprintf("java-%d-%s", version, suffix))
_, err := os.Stat(path)
if err == nil {
return true
}
return false
}
func (j *JavaManager)InstallJavaVer(version int) {
suffix := "lin.tar.gz"
if runtime.GOOS == "windows" {
suffix = "win.zip"
}
buff := new(bytes.Buffer)
HttpDownload("java/"+fmt.Sprintf("java-%d-%s", version, suffix), buff, j.app.Ctx)
path, _ := os.UserConfigDir()
suffix = "lin"
if runtime.GOOS == "windows" {
suffix = "win"
}
path = filepath.Join(path, "FCLauncher", "java", fmt.Sprintf("java-%d-%s", version, suffix))
os.MkdirAll(path, 0755)
if runtime.GOOS == "windows" {
zr := zipstream.NewReader(buff)
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
return
}
target := filepath.Join(path, strings.SplitN(entry.Name, "/", 2)[1])
if !entry.IsDir() {
rc, err := entry.Open()
if err != nil {
return
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, entry.FileInfo().Mode())
if err != nil {
return
}
if _, err := io.Copy(f, rc); err != nil {
return
}
f.Close()
rc.Close()
} else {
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
}
}
} else {
gzip, _ := gzip.NewReader(buff)
defer gzip.Close()
tr := tar.NewReader(gzip)
out:
for {
header, err := tr.Next()
switch {
case err == io.EOF:
break out
case err != nil:
return
case header == nil:
continue
}
target := filepath.Join(path, strings.SplitN(header.Name, string(os.PathSeparator), 2)[1])
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return
}
if _, err := io.Copy(f, tr); err != nil {
return
}
f.Close()
}
}
}
}

63
fclauncher/Modpack.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"time"
)
type Modpack struct {
Name string
Id string
Last_updated string
Versions []Version
}
type Version struct {
Version string
Data time.Time
File string
}
type ModpackManager struct {
app *App
Modpacks []Modpack
}
func (m *ModpackManager) QuerryModpacks() {
m.Modpacks = []Modpack{}
buff := new(bytes.Buffer)
err := HttpDownload("modpacks.json", buff, nil)
if err != nil {
fmt.Printf("HTTP error: %s\n", err)
return
}
err = json.Unmarshal(buff.Bytes(), &m.Modpacks)
if err != nil {
return
}
for ind, pack := range m.Modpacks {
buff = new(bytes.Buffer)
err = HttpDownload(pack.Id+"/versions.json", buff, nil)
if err != nil {
continue
}
json.Unmarshal(buff.Bytes(), &pack.Versions)
m.Modpacks[ind] = pack
}
}
func (m *ModpackManager) GetModpacks() []Modpack {
return m.Modpacks
}
func (m *ModpackManager) GetModpack(id string) Modpack {
for _, pack := range m.Modpacks {
if pack.Id == id {
return pack
}
}
return Modpack{}
}

170
fclauncher/Prism.go Normal file
View File

@ -0,0 +1,170 @@
package main
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/zhyee/zipstream"
)
type Prism struct {
app *App
}
func (Prism) CheckInstalled() bool {
path, _ := os.UserConfigDir()
_, err := os.Stat(filepath.Join(path, "FCLauncher", "prism"))
if err == nil {
return true
} else {
return false
}
}
func (p *Prism) Install() {
suffix := "lin.tar.gz"
shortSuffix := "lin"
if runtime.GOOS == "windows" {
suffix = "win.zip"
shortSuffix = "win"
}
buff := new(bytes.Buffer)
HttpDownload("prism/prism-"+suffix, buff, p.app.Ctx)
path, _ := os.UserConfigDir()
os.MkdirAll(filepath.Join(path, "FCLauncher", "prism"), 0755)
if runtime.GOOS == "windows" {
zr := zipstream.NewReader(buff)
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
return
}
target := filepath.Join(path, "FCLauncher", "prism", entry.Name)
if !entry.IsDir() {
rc, err := entry.Open()
if err != nil {
return
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, entry.FileInfo().Mode())
if err != nil {
return
}
if _, err := io.Copy(f, rc); err != nil {
return
}
f.Close()
rc.Close()
} else {
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
}
}
} else {
gzip, _ := gzip.NewReader(buff)
defer gzip.Close()
tr := tar.NewReader(gzip)
out:
for {
header, err := tr.Next()
switch {
case err == io.EOF:
break out
case err != nil:
return
case header == nil:
continue
}
target := filepath.Join(path, "FCLauncher", "prism", header.Name)
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return
}
if _, err := io.Copy(f, tr); err != nil {
return
}
f.Close()
}
}
}
dir, _ := os.UserConfigDir()
buff = new(bytes.Buffer)
HttpDownload("prism/prismlauncher.cfg", buff, nil)
scanner := bufio.NewScanner(buff)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "prism", "prismlauncher.cfg"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "JavaPath") {
line = fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-21-%s", strings.ReplaceAll(dir, "\\", "/"), shortSuffix)
}
if strings.HasPrefix(line, "LastHostname") {
host, _ := os.Hostname()
line = fmt.Sprintf("LastHostname=%s", host)
}
f.WriteString(line + "\n")
}
}
func (Prism)GetInstanceDir() string{
dir, _ := os.UserConfigDir()
return filepath.Join(dir, "FCLauncher", "prism", "instances")
}
func (Prism)getExecutableName() string {
execName := "PrismLauncher"
if runtime.GOOS == "windows" {
execName = "prismlauncher.exe"
}
return execName
}
func (p *Prism)ImportModpack(path string) {
dir, _ := os.UserConfigDir()
child := exec.Command(filepath.Join(dir, "FCLauncher", "prism", p.getExecutableName()), "-I", path)
child.Start()
tokens := strings.Split(path, string(os.PathSeparator))
versionPath := filepath.Join(dir, "FCLauncher", "prism", "instances",strings.Split(tokens[len(tokens)-1], ".")[0], ".minecraft", "version.txt")
for {
time.Sleep(time.Second * 3)
if _, err := os.Stat(versionPath); err == nil {
break
}
}
child.Process.Kill()
}
func (p *Prism)LaunchInstance(instance Instance) {
p.app.Status(fmt.Sprintf("Launching %s", instance.InstanceName))
dir, _ := os.UserConfigDir()
child := exec.Command(filepath.Join(dir, "FCLauncher", "prism", p.getExecutableName()), "-l", instance.InstanceName)
child.Start()
}

16
fclauncher/README.md Normal file
View File

@ -0,0 +1,16 @@
# README
## About
This is the official Wails Svelte-TS template.
## Live Development
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
to this in your browser, and you can call your Go code from devtools.
## Building
To build a redistributable, production mode package, use `wails build`.

158
fclauncher/app.go Normal file
View File

@ -0,0 +1,158 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/inconshreveable/go-update"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
const client_id string = "9305aeb8-5ecb-4e7a-b28f-c33aefcfbd8d"
const client_version string = "0.0.7"
type LauncherMetadata struct {
Schema_Version string
Version string
Desc string
Downloads map[string]string
}
// App struct
type App struct {
Ctx context.Context
PrismLauncher Prism
Java JavaManager
Instance InstanceManager
Modpacks ModpackManager
Auth authenticationResp
}
// NewApp creates a new App application struct
func NewApp() *App {
a := &App{}
a.Java = JavaManager{app: a}
a.Instance = InstanceManager{app: a}
a.Modpacks = ModpackManager{app: a}
return a
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.Ctx = ctx
}
// Greet returns a greeting for the given name
func openbrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func (a *App) CheckPrerequisites() {
buff := new(bytes.Buffer)
err := HttpDownload("launcher.json", buff, nil)
fmt.Printf("Starting\n")
if err == nil {
data, _ := io.ReadAll(buff)
meta := LauncherMetadata{}
json.Unmarshal(data, &meta)
if client_version != meta.Version {
//Update available!
val, _ := wruntime.MessageDialog(a.Ctx, wruntime.MessageDialogOptions{Type: wruntime.QuestionDialog, Title: "Update!", Message: fmt.Sprintf("There is an update available:\n\n%s -> %s\n\nUpdate Description:\n\n%s\n\nWould you like to update?\n", client_version, meta.Version, meta.Desc)})
if val == "Yes" {
//run the update
fmt.Printf("Updating\n")
buff := new(bytes.Buffer)
HttpDownload(meta.Downloads[runtime.GOOS], buff, nil)
executable, _ := os.Executable()
err := update.Apply(buff, update.Options{})
if err != nil {
fmt.Printf("Error!")
}
child := exec.Command(executable)
err = child.Start()
if err != nil {
fmt.Printf("Unable to launch: %s\n", err)
}
wruntime.Quit(a.Ctx)
}
}
}
a.Status("Querrying Existing Instances")
a.Instance.SearchInstances()
a.Status("Pulling Modpacks")
a.Modpacks.QuerryModpacks()
a.Status("Logging in with Microsoft")
dir, _ := os.UserConfigDir()
authenticated := false
if _, err := os.Stat(filepath.Join(dir, "FCLauncher", "authentication.json")); err == nil {
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "authentication.json"), os.O_RDONLY, 0755)
defer f.Close()
data, _ := io.ReadAll(f)
json.Unmarshal(data, &a.Auth)
a.Auth, err = TokenRefresh(*a, a.Auth)
if err == nil {
authenticated = true
} else {
fmt.Printf("token reauth failed, requesting device code: %s\n", err)
}
}
if !authenticated {
var err error
//a.Auth, err = AuthCode(*a)
a.Auth, err = OAuth2(*a)
if err != nil {
fmt.Printf("Authentication Error: %s\n", err)
return
}
}
os.MkdirAll(filepath.Join(dir, "FCLauncher"), 0755)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "authentication.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
defer f.Close()
data, _ := json.Marshal(a.Auth)
f.Write(data)
}
func (App) GetVersions() ([]string, error) {
manifest, err := GetVersionManifest()
if err != nil {
fmt.Printf("Manifest Error: %s\n", err)
return []string{}, err
}
versions := []string{}
for _, version := range manifest.Versions {
versions = append(versions, version.Id)
}
return versions, nil
}
func (a *App) Status(status string) {
fmt.Printf("LOG: %s\n", status)
wruntime.EventsEmit(a.Ctx, "status", status)
}

268
fclauncher/auth.go Normal file
View File

@ -0,0 +1,268 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type LauncherAuth struct {
Id string
Name string
Token string
}
type McProfile struct {
Id string
Name string
}
type devcodeResp struct {
User_code string
Device_code string
Verification_uri string
Expires_in string
Interval int
Message string
}
type authenticationResp struct {
Access_token string
Token_type string
Refresh_token string
Expires_in string
Error string
Error_description string
}
type xboxAuthProperties struct {
AuthMethod string
SiteName string
RpsTicket string
}
type xboxAuthRequest struct {
Properties xboxAuthProperties
RelyingParty string
TokenType string
}
type xboxDisplayClaim struct {
Uhs string
Gtg string
Xid string
Agg string
Usr string
Utr string
Prv string
}
type xboxDisplayClaims struct {
Xui []xboxDisplayClaim
}
type xboxAuthResponse struct {
IssueInstant time.Time
NotAfter time.Time
Token string
DisplayClaims xboxDisplayClaims
}
type XSTSProperties struct {
SandboxId string
UserTokens []string
}
type XSTSRequest struct {
Properties XSTSProperties
RelyingParty string
TokenType string
}
type McAuthRequest struct {
Xtoken string `json:"xtoken"`
Platform string `json:"platform"`
}
type McAuthResponse struct {
Username string
Access_token string
Expires_in string
Token_type string
}
func getHTTPRedirect(w http.ResponseWriter, r *http.Request, srv *http.Server, code *string) {
r.ParseForm()
fmt.Printf("Response Code: %s\n", r.Form.Get("code"))
if r.Form.Get("code") != "" {
*code = r.Form.Get("code")
io.WriteString(w, "You can now close this window and return to the application.")
} else {
srv.Shutdown(r.Context())
}
}
func AuthCode(a App) (authenticationResp, error) {
authentication := authenticationResp{}
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", url.Values{
"client_id": {client_id},
"scope": {"XboxLive.SignIn XboxLive.offline_access"},
})
if err != nil {
return authentication, fmt.Errorf("Unable to request device code: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return authentication, fmt.Errorf("Unable to request device code: %s\n", resp.Status)
}
data, _ := io.ReadAll(resp.Body)
codeResp := devcodeResp{}
json.Unmarshal(data, &codeResp)
//display message
fmt.Printf("resp: %s\n", data)
openbrowser(codeResp.Verification_uri)
wruntime.MessageDialog(a.Ctx, wruntime.MessageDialogOptions{Type: wruntime.InfoDialog, Title: "Authentication", Message: codeResp.Message + ". The code has been automatically coppied to the clipboard."})
wruntime.ClipboardSetText(a.Ctx, codeResp.Device_code)
ticker := time.NewTicker(time.Second * time.Duration(codeResp.Interval))
for range ticker.C {
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"client_id": {client_id},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {codeResp.Device_code},
})
if err != nil {
return authentication, fmt.Errorf("Authentication request error %e\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
json.Unmarshal(data, &authentication)
if authentication.Error == "" {
return authentication, nil
}
}
return authentication, fmt.Errorf("Unknown error")
}
func OAuth2(a App) (authenticationResp, error) {
code := "code"
srv := http.Server{Addr: ":5000"}
authentication := authenticationResp{}
verifier := make([]byte, 128)
rand.Read(verifier)
verifier_string := base64.RawURLEncoding.EncodeToString(verifier)
challenge := sha256.Sum256([]byte(verifier_string))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { getHTTPRedirect(w, r, &srv, &code) })
openbrowser("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=" + client_id + "&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A5000&response_mode=query&scope=XboxLive.signin&state=12345&code_challenge=" + base64.RawURLEncoding.EncodeToString(challenge[:]) + "&code_challenge_method=S256&prompt=login")
srv.ListenAndServe()
fmt.Printf("continuing auth\n")
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"redirect_uri": {"http://127.0.0.1:5000"},
"code_verifier": {verifier_string},
"client_id": {client_id},
})
if err != nil {
return authenticationResp{}, fmt.Errorf("unable to request token: %e", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
json.Unmarshal(data, &authentication)
//fmt.Printf("auth data: %s\n", data)
return authentication, nil
}
func TokenRefresh(app App, auth authenticationResp) (authenticationResp, error) {
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"client_id": {client_id},
"grant_type": {"refresh_token"},
"refresh_token": {auth.Refresh_token},
"scope": {"XboxLive.SignIn XboxLive.offline_access"},
})
if err != nil {
return authenticationResp{}, fmt.Errorf("unable to refresh token: %e\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
authResp := authenticationResp{}
json.Unmarshal(data, &authResp)
if authResp.Error != "" {
return authResp, fmt.Errorf("unable to request new token: %s", authResp.Error_description)
}
return authResp, nil
}
func MicrosoftAuth(auth authenticationResp) (LauncherAuth, error) {
//Xbox Live Auth
req, _ := json.Marshal(xboxAuthRequest{Properties: xboxAuthProperties{AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", RpsTicket: "d=" + auth.Access_token}, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT"})
client := http.Client{}
httpreq, _ := http.NewRequest("POST", "https://user.auth.xboxlive.com/user/authenticate", bytes.NewBuffer(req))
httpreq.Header.Add("x-xbl-contract-version", "1")
httpreq.Header.Add("Content-Type", "application/json")
httpreq.Header.Add("Accept", "application/json")
httpResp, err := client.Do(httpreq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain xbox live token: %e\n", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain xbox live token: %s\n", httpResp.Status)
}
d, _ := io.ReadAll(httpResp.Body)
xblAuth := xboxAuthResponse{}
json.Unmarshal(d, &xblAuth)
xstsData, _ := json.Marshal(XSTSRequest{Properties: XSTSProperties{SandboxId: "RETAIL", UserTokens: []string{xblAuth.Token}}, RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT"})
httpXstsReq, _ := http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewBuffer(xstsData))
httpXstsReq.Header.Add("Content-Type", "application/json")
httpResp, err = client.Do(httpXstsReq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain minecraft sts token: %e\n", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain minecraft sts token: %s\n", httpResp.Status)
}
d, _ = io.ReadAll(httpResp.Body)
mcApi := xboxAuthResponse{}
json.Unmarshal(d, &mcApi)
mcAuthData, _ := json.Marshal(McAuthRequest{Xtoken: "XBL 3.0 x=" + mcApi.DisplayClaims.Xui[0].Uhs + ";" + mcApi.Token, Platform: "PC_LAUNCHER"})
httpReqMC, _ := http.NewRequest("POST", "https://api.minecraftservices.com/launcher/login", bytes.NewBuffer(mcAuthData))
httpReqMC.Header.Add("Content-Type", "application/json")
httpReqMC.Header.Add("Accept", "application/json")
resp, err := client.Do(httpReqMC)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain mojang auth token: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain mojang auth token: %s\n", resp.Status)
}
d, _ = io.ReadAll(resp.Body)
mcAuth := McAuthResponse{}
json.Unmarshal(d, &mcAuth)
httpreq, err = http.NewRequest("GET", "https://api.minecraftservices.com/minecraft/profile", new(bytes.Buffer))
httpreq.Header.Add("Content-Type", "application/json")
httpreq.Header.Add("Accept", "application/json")
httpreq.Header.Add("Authorization", "Bearer "+mcAuth.Access_token)
resp, _ = client.Do(httpreq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to get profile data: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to get profile data: %s\n", resp.Status)
}
data, _ := io.ReadAll(resp.Body)
profile := McProfile{}
json.Unmarshal(data, &profile)
return LauncherAuth{Id: profile.Id, Name: profile.Name, Token: mcAuth.Access_token}, nil
}

View File

@ -0,0 +1,35 @@
# Build Directory
The build directory is used to house all the build files and assets for your application.
The structure is:
* bin - Output directory
* darwin - macOS specific files
* windows - Windows specific files
## Mac
The `darwin` directory holds files specific to Mac builds.
These may be customised and used as part of the build. To return these files to the default state, simply delete them
and
build with `wails build`.
The directory contains the following files:
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
## Windows
The `windows` directory contains the manifest and rc files used when building with `wails build`.
These may be customised for your application. To return these files to the default state, simply delete them and
build with `wails build`.
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
will be created using the `appicon.png` file in the build directory.
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
as well as the application itself (right click the exe -> properties -> details)
- `wails.exe.manifest` - The main application manifest file.

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

@ -0,0 +1,68 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,63 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}

View File

@ -0,0 +1,115 @@
Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"
# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0"
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true
!include "MUI.nsh"
!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'
Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.
Function .onInit
!insertmacro wails.checkArchitecture
FunctionEnd
Section
!insertmacro wails.setShellContext
!insertmacro wails.webview2runtime
SetOutPath $INSTDIR
!insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
AccessControl::GrantOnFile "$INSTDIR" "(BU)" "FullAccess"
SectionEnd
Section "uninstall"
!insertmacro wails.setShellContext
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd

View File

@ -0,0 +1,236 @@
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "fclauncher"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "fclauncher"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "fclauncher"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "Copyright........."
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
!macro wails.setShellContext
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
SetShellVarContext all
${else}
SetShellVarContext current
${EndIf}
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "tmp\MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro wails.associateFiles
; Create file associations
!macroend
!macro wails.unassociateFiles
; Delete app associations
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
!macroend

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

145
fclauncher/fabric.go Normal file
View File

@ -0,0 +1,145 @@
package main
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type Fabric struct {
}
type FabricDefinition struct {
Separator string
Build int
Maven string
Version string
Stable bool
}
type FabricLibrary struct {
Name string
Url string
Sha1 string
}
type FabricLibraries struct {
Client []FabricLibrary
Common []FabricLibrary
Server []FabricLibrary
}
type FabricMeta struct {
Version int
Libraries FabricLibraries
MainClass map[string]string
}
type FabricVersion struct {
Loader FabricDefinition
Intermediary FabricDefinition
LauncherMeta FabricMeta
}
func (Fabric) GetFabricVersions(mcVersion string) ([]FabricVersion, error) {
resp, err := http.Get("https://meta.fabricmc.net/v2/versions/loader/" + mcVersion)
if err != nil {
return []FabricVersion{}, fmt.Errorf("Unable to pull fabric version manifest: %s\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
versions := []FabricVersion{}
json.Unmarshal(data, &versions)
return versions, nil
}
func GetFabricMetadata(mcVersion string, fabricVersion string) (FabricVersion, error) {
versions, err := Fabric{}.GetFabricVersions(mcVersion)
if err != nil {
return FabricVersion{}, fmt.Errorf("unable to download versions manifest: %e\n", err)
}
for _, version := range versions {
if version.Loader.Version == fabricVersion {
return version, nil
}
}
return FabricVersion{}, fmt.Errorf("Unable to find requested version.\n")
}
func InstallLib(lib FabricLibrary, libDir string, a *App) {
a.Status(fmt.Sprintf("Checking %s\n", lib.Name))
path := filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
if _, err := os.Stat(filepath.Join(libDir, path)); err == nil {
f, _ := os.OpenFile(filepath.Join(libDir, path), os.O_RDONLY, 0755)
defer f.Close()
data, _ := io.ReadAll(f)
sha := sha1.Sum(data)
if hex.EncodeToString(sha[:20]) == lib.Sha1 {
return
}
}
a.Status(fmt.Sprintf("Downloading %s\n", lib.Name))
unixPath := strings.ReplaceAll(path, "\\", "/")
resp, err := http.Get(lib.Url + unixPath)
if err != nil {
return
}
defer resp.Body.Close()
buff := new(bytes.Buffer)
downloaded := 0
for {
count, err := io.CopyN(buff, resp.Body, BlockSize)
if err == io.EOF {
downloaded += int(count)
break
}
if err != nil {
fmt.Printf("Error Downloading libs: %e\n", err)
return
}
downloaded += int(count)
wruntime.EventsEmit(a.Ctx, "download", downloaded, resp.ContentLength)
}
os.MkdirAll(filepath.Join(libDir, ProcessMavenPath(lib.Name)), 0755)
f, _ := os.OpenFile(filepath.Join(libDir, path), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
io.Copy(f, buff)
wruntime.EventsEmit(a.Ctx, "download_complete")
}
func InstallFabricLibs(mcVersion string, fabricVersion string, libDir string, a *App) {
metadata, _ := GetFabricMetadata(mcVersion, fabricVersion)
for _, lib := range metadata.LauncherMeta.Libraries.Client {
InstallLib(lib, libDir, a)
}
for _, lib := range metadata.LauncherMeta.Libraries.Common {
InstallLib(lib, libDir, a)
}
InstallLib(FabricLibrary{Name: metadata.Loader.Maven, Sha1: "", Url: "https://maven.fabricmc.net/"}, libDir, a)
InstallLib(FabricLibrary{Name: metadata.Intermediary.Maven, Sha1: "", Url: "https://maven.fabricmc.net/"}, libDir, a)
}
func ProcessMavenPath(maven string) string {
tokens := strings.Split(maven, ":")
path := filepath.Join(strings.Split(tokens[0], ".")...)
pack := tokens[1]
version := tokens[2]
path = filepath.Join(path, pack, version)
return path
}
func ProcessMavenFilename(maven string) string {
tokens := strings.Split(maven, ":")
pack := tokens[1]
version := tokens[2]
return pack + "-" + version + ".jar"
}

185
fclauncher/forge.go Normal file
View File

@ -0,0 +1,185 @@
package main
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/zhyee/zipstream"
)
type Forge struct{}
type ForgeVersion struct {
Version string
Time string
Url string
}
type ForgeLibraryArtifact struct {
Path string
Url string
Sha1 string
}
type ForgeLibraryDownload struct {
Artifact ForgeLibraryArtifact
}
type ForgeLibrary struct {
Name string
Downloads ForgeLibraryDownload
}
type ForgeInstallData struct {
Id string
Time time.Time
ReleaseTime time.Time
InheritsFrom string
Type string
MainClass string
Libraries []ForgeLibrary
}
func parseForgeVersions(html string) []ForgeVersion {
lines := strings.Split(html, "\n")
parsing := false
foundTR := false
buff := ""
versions := []ForgeVersion{}
for _, line := range lines {
if strings.Contains(line, "<tbody>") {
parsing = true
} else if strings.Contains(line, "</tbody>") {
parsing = false
} else if parsing {
if strings.Contains(line, "<tr>") {
buff = ""
foundTR = true
} else if strings.Contains(line, "</tr>") {
foundTR = false
versions = append(versions, parseForgeVersion(buff))
} else if foundTR {
buff += line + "\n"
}
}
}
return versions
}
func parseForgeVersion(html string) ForgeVersion {
lines := strings.Split(html, "\n")
version := ForgeVersion{}
for ind, line := range lines {
if strings.Contains(line, "<td class=\"download-version\">") {
version.Version = strings.TrimSpace(lines[ind+1])
} else if strings.Contains(line, "<td class=\"download-time\"") {
version.Time = strings.Split(strings.Split(line, "<td class=\"download-time\" title=\"")[1], "\">")[0]
} else if strings.Contains(line, "https://adfoc.us") && strings.Contains(line, "installer.jar") {
version.Url = strings.Split(strings.Split(line, "&url=")[1], "\">")[0]
}
}
return version
}
func (Forge) GetForgeVersions(mcVersion string) ([]ForgeVersion, error) {
resp, err := http.Get(fmt.Sprintf("https://files.minecraftforge.net/net/minecraftforge/forge/index_%s.html", mcVersion))
if err != nil {
return []ForgeVersion{}, fmt.Errorf("unable to access minecraft forge index: %e", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return []ForgeVersion{}, fmt.Errorf("unable to access minecraft forge index: %s", err)
}
data, _ := io.ReadAll(resp.Body)
return parseForgeVersions(string(data)), nil
}
func GetForgeInstallDataFromVersion(version ForgeVersion) (ForgeInstallData, error) {
resp, err := http.Get(version.Url)
if err != nil {
return ForgeInstallData{}, fmt.Errorf("unable to pull jar file: %e", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return ForgeInstallData{}, fmt.Errorf("unable to pull jar file: %s", resp.Status)
}
zs := zipstream.NewReader(resp.Body)
for {
entry, err := zs.GetNextEntry()
if err == io.EOF {
break
}
if entry.Name != "version.json" {
continue
}
f, _ := entry.Open()
defer f.Close()
data, _ := io.ReadAll(f)
installData := ForgeInstallData{}
json.Unmarshal(data, &installData)
return installData, nil
}
return ForgeInstallData{}, fmt.Errorf("unable to find version.json")
}
func GetForgeInstallData(mcVersion string, forgeVersion string) (ForgeInstallData, error) {
versions, err := Forge{}.GetForgeVersions(mcVersion)
if err != nil {
return ForgeInstallData{}, fmt.Errorf("failed to pull forge versions: %e", err)
}
for _, version := range versions {
if version.Version == forgeVersion {
return GetForgeInstallDataFromVersion(version)
}
}
return ForgeInstallData{}, fmt.Errorf("unable to find the requested version")
}
func InstallForgeLibs(mcVersion string, forgeVersion string, libDir string) {
installData, err := GetForgeInstallData(mcVersion, forgeVersion)
if err != nil {
fmt.Printf("Unable to get install data: %s\n", err)
}
for _, lib := range installData.Libraries {
if _, err := os.Stat(filepath.Join(libDir, lib.Downloads.Artifact.Path)); err == nil {
if f, err := os.OpenFile(filepath.Join(libDir, lib.Downloads.Artifact.Path), os.O_RDONLY, 0755); err == nil {
defer f.Close()
data, _ := io.ReadAll(f)
sha := sha1.Sum(data)
if hex.EncodeToString(sha[:20]) == lib.Downloads.Artifact.Sha1 {
continue
}
}
}
resp, err := http.Get(lib.Downloads.Artifact.Url)
if err != nil {
fmt.Printf("Unable to download library %s: %s\n", lib.Name, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("Unable to download library %s: %s\n", lib.Name, resp.Status)
continue
}
tokens := strings.Split(lib.Downloads.Artifact.Path, "/")
path := ""
for ind, token := range tokens {
if ind == len(tokens)-1 {
break
}
path = filepath.Join(path, token)
}
os.MkdirAll(filepath.Join(libDir, path), 0755)
f, _ := os.OpenFile(filepath.Join(libDir, lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
io.Copy(f, resp.Body)
}
}

View File

@ -0,0 +1,5 @@
{
"recommendations": [
"svelte.svelte-vscode"
]
}

View File

@ -0,0 +1,65 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/)
+ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its
serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less,
and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account
the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the
other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte
project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been
structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash
references keeps the default TypeScript setting of accepting type information from the entire workspace, while also
adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to
install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of
JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds:
not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing
JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr`
and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the
details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be
replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>fclauncher</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
</body>
</html>

1613
fclauncher/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.7"
}
}

View File

@ -0,0 +1 @@
48cb20b8d107dab0a7876a449352234a

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { AdminAuth } from '../wailsjs/go/main/Admin.js'
import { adminLogin } from './global';
var username = "", password = ""
var loginFail = false
function login() {
AdminAuth(username, password).then((result) => {
if(result) {
$adminLogin = true
} else {
loginFail = true
}
})
}
</script>
<main>
<div class="container">
<input type="text" placeholder="Username" bind:value={username} />
<input type="password" placeholder="Password" bind:value={password} />
</div>
{#if loginFail}
<p>Login Failed!</p>
{/if}
<button on:click={login}>Login</button>
</main>
<style>
* {
box-sizing: border-box;
font-family: sans-serif;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-primary: #23232e;
--bg-secondary: #141418;
}
p {
color: red;
font-weight: bold;
}
.container{
display: flex;
justify-content: center;
margin: 3rem;
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import AdminSettings from "./AdminSettings.svelte";
import AdminLogin from "./AdminLogin.svelte";
import { adminLogin } from "./global";
</script>
<main>
<h1>Admin</h1>
<div class="container">
{#if $adminLogin}
<AdminSettings />
{:else}
<AdminLogin />
{/if}
</div>
</main>
<style>
* {
box-sizing: border-box;
font-family: sans-serif;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-primary: #23232e;
--bg-secondary: #141418;
}
.container{
display: flex;
justify-content: center;
margin: 3rem;
}
</style>

View File

@ -0,0 +1,87 @@
<script lang="ts">
import {onMount} from 'svelte'
import { GetModpacks, QuerryModpacks } from '../wailsjs/go/main/ModpackManager';
var modpacks = []
var selectedPack = []
onMount(() => {
QuerryModpacks().then(() => {
GetModpacks().then((result) => {
modpacks = result
selectedPack = modpacks[0]
})
})
})
function select(modpack) {
selectedPack = modpack
}
</script>
<main>
<p>Selectec pack: {selectedPack.Name}</p>
<div class="container">
<div class="modpackList">
{#each modpacks as pack}
{#if pack == selectedPack}
<button on:click={select(pack)} class="modpackElementSelected">{pack.Name}</button>
{:else}
<button on:click={select(pack)} class="modpackElement">{pack.Name}</button>
{/if}
{/each}
</div>
<div class="modpackOptions">
</div>
</div>
</main>
<style>
* {
display: box;
box-sizing: border-box;
font-family: sans-serif;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-primary: #23232e;
--bg-secondary: #141418;
width: 100%;
}
.container{
display: flex;
justify-content: left;
margin: 3rem;
}
.modpackList {
display: flex;
flex-direction: column;
background-color: #141418;
float: left;
width: 60%;
height: 20em;
align-items: center;
gap: 2px;
}
.modpackList button {
border: none;
color: inherit;
}
.modpackElement {
width: 100%;
background-color: #23232e;
}
.modpackElement:hover {
background-color: #303030;
}
.modpackElementSelected {
width: 100%;
background-color: #707070;
}
.modpackElementSelected:hover {
background-color: #303030;
}
</style>

View File

@ -0,0 +1,139 @@
<script lang="ts">
import logo from './assets/images/fc-logo.png'
import Instances from './Instances.svelte'
import Loading from './Loading.svelte'
import Modpacks from './Modpacks.svelte'
import {CheckPrerequisites} from '../wailsjs/go/main/App.js'
import { onMount } from 'svelte'
import { loading, currentPage, instances, themecolor } from './global'
import { slide } from 'svelte/transition'
import Navbar from './Navbar.svelte'
import Instancepage from './Instancepage.svelte'
import { set_attributes, set_style } from 'svelte/internal';
import {GetInstances} from '../wailsjs/go/main/InstanceManager.js'
import Settingspage from './Settingspage.svelte';
import AdminPage from './AdminPage.svelte';
let width: number = 10
let navMargin = document.getElementById("body") as HTMLElement;
let r
function UpdateInstances() {
$loading = true
GetInstances().then((result) => {
$instances = result
$loading = false
})
}
onMount(() => {
CheckPrerequisites().then(() => {
UpdateInstances()
})
r = document.getElementById('wrapper');
})
function setMargin(){
r.style.setProperty('--navMargin', '17rem');
}
function unsetMargin(){
r.style.setProperty('--navMargin', '5rem');
}
function initialColor() {
r.style.setProperty('--accent-color', 'purple');
}
function setcolor() {
console.log("changing theme");
r.style.setProperty('--accent-color', $themecolor);
}
window.document.onload = function() {setcolor()};
</script>
<main>
<div id = "wrapper">
<div class="navbar" on:mouseover={setMargin} on:focus={setMargin} on:mouseleave={unsetMargin} >
<Navbar />
</div>
<body class="body" id="body">
<!--<img alt="Wails logo" id="logo" src="{logo}">-->
{#if $loading}
<div transition:slide="{{duration:100}}" class="central">
<Loading />
</div>
{:else if $currentPage == 1}
<div transition:slide="{{duration:100}}" class="central">
<Instancepage on:change-theme = {setcolor} />
</div>
{:else if $currentPage == 2}
<div transition:slide="{{duration:100}}" class="central">
<Instances UpdateInstances={UpdateInstances} />
</div>
{:else if $currentPage == 3}
<div transition:slide="{{duration:100}}" class="central">
<Modpacks UpdateInstances={UpdateInstances} />
</div>
{:else if $currentPage == 4}
<div transition:slide="{{duration:100}}" class="central">
<Settingspage on:change-theme = {setcolor} on:change-theme-back = {initialColor} />
</div>
{:else if $currentPage == 5}
<div transition:slide="{{duration:100}}" class="central">
<AdminPage />
</div>
{/if}
</body>
</div>
</main>
<style>
:root{
font-size: 16px;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-secondary: #2c2c33;
--bg-primary: #141418;
background-color: var(--bg-secondary);
}
#wrapper{
--accent-color: purple;
--navMargin: 5rem;
}
.navbar{
z-index: 5;
}
#logo {
display: flex;
width: 70%;
height: 70%;
margin: auto;
padding: 10% 0 0;
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
background-origin: content-box;
}
body{
display: flex;
flex-direction: column;
margin-left: var(--navMargin);
transition: 200ms;
}
main{
background-color: var(--bg-secondary);
}
.central{
background-color: var(--bg-secondary);
}
</style>

View File

@ -0,0 +1,194 @@
<script lang="ts">
import {instances, loading, navMargin} from './global'
import {OpenInstanceFolder, InstallVanilla, LaunchInstance, GetInstances, InstallForge, InstallQuilt, InstallFabric, CheckUpdate, DeleteInstance} from '../wailsjs/go/main/InstanceManager.js'
import {GetVersions} from '../wailsjs/go/main/App.js'
import {onMount, createEventDispatcher} from 'svelte'
var testArray = ["test","test2","test3"];
export let UpdateInstances
let pack: string = "";
let instance: string
let radio: string = "";
let marginScale: string= $navMargin + "rem";
var r = document.querySelector('main');
const dispatch = createEventDispatcher()
function changetheme(){
dispatch('change-theme');
}
//function initialColor() {
// r.style.setProperty('--accent-color', 'purple');
//}
//window.document.onload = function() {initialColor()};
function launchclick(event) {
$loading = true
LaunchInstance(radio).then(() => {
$loading = false
})
}
function deleteclick() {
$loading = true
DeleteInstance(radio).then(() => {
GetInstances().then((result) => {
$instances = result
$loading = false
})
})
}
</script>
<main>
<div class="instance-header">Instances</div>
<div class="header">
<div class="container">
<div class="tile-group">
{#each $instances as instance}
<div class="input-container" id=input-container>
<input id={instance} bind:group={radio} type="radio" name="radio" value={instance}>
<div class="radio-tile">
<!--icon goes here later-->
<label for={instance}>{instance}</label>
</div>
</div>
{/each}
</div>
</div>
<div class="options-container">
<button class="instance-button" disabled='{radio == ""}' on:click={launchclick}>Launch {radio}</button>
<button class="instance-button" disabled='{radio == ""}' on:click={() => {OpenInstanceFolder(radio)}}>Open Instance Folder</button>
<button class="instance-button" disabled='{radio == ""}' on:click={deleteclick}>Delete Instance</button>
</div>
</div>
</main>
<style>
* {
box-sizing: border-box;
font-family: sans-serif;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-primary: #23232e;
--bg-secondary: #141418;
}
body{
background-color: var(--bg-secondary);
}
main{
display: flex;
flex-direction: column;
}
.header{
display: flex;
flex: 3;
flex-direction: row;
}
.instance-header{
display: inline-block;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 1rem;
text-align: center;
font-size: 1.5rem;
color: var(--text-primary);
}
.options-container{
display: flex;
height: 100%;
justify-self: right;
justify-content: center;
align-items: center;
width: 15rem;
flex-direction: column;
position: relative;
margin-left: auto;
margin-top: 15rem;
gap: 10px;
}
.instance-button{
min-height: 3rem;
border:solid;
border-radius: 4px;
background-color: var(--bg-secondary);
border-color: var(--bg-secondary);
color: var(--text-primary);
font-size: large;
max-width: 10rem;
max-height: fit-content;
}
.instance-button:hover:enabled{
color: var(--text-secondary);
}
.instance-button:active:enabled{
opacity: 0.6;
}
.instance-button:disabled{
opacity: 0.6;
}
.container{
display: flex;
min-height: fit-content;
}
.tile-group{
display: flex;
flex-wrap: wrap;
flex: 1;
flex-grow:1 ;
}
.input-container{
position: relative;
height: 7rem;
width: 7rem;
margin: 0.5rem;
}
.input-container input{
position: absolute;
height: 100%;
width: 100%;
margin-left: -3.5rem;
z-index: 0;
opacity: 0;
cursor: pointer;
}
.input-container .radio-tile{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
border: 2px solid var(--accent-color);
border-radius: 8px;
transition: all 300ms ease;
}
.input-container label{
color: var(--accent-color);
font-size: 1.2rem;
font-weight: 600;
}
input:checked + .radio-tile{
background: var(--accent-color);
box-shadow: 0 0 12px var(--accent-color);
}
input:hover + .radio-tile{
box-shadow: 0 0 12px var(--accent-color);
}
input:checked + .radio-tile label{
color: var(--text-primary);
}
</style>

View File

@ -0,0 +1,198 @@
<script lang="ts">
import {InstallVanilla, LaunchInstance, GetInstances, InstallForge, InstallQuilt, InstallFabric, CheckUpdate} from '../wailsjs/go/main/InstanceManager.js'
import {GetVersions} from '../wailsjs/go/main/App.js'
import {GetFabricVersions} from '../wailsjs/go/main/Fabric.js'
import {GetQuiltVersions} from '../wailsjs/go/main/Quilt.js'
import {GetForgeVersions} from '../wailsjs/go/main/Forge.js'
import {onMount} from 'svelte'
import {loading, addingInstance} from './global'
import {slide} from 'svelte/transition'
let modpacks: string[] = []
let pack: string
//let instances: Instance[] = []
export let UpdateInstances
let name: string = "New Modpack"
let loader: string = "none"
let fabric_ver: string = ""
let fab_versions: string[] = []
let quilt_ver: string = ""
let quilt_versions: string[] = []
let forge_ver: string = ""
let forge_versions: string[] = []
function updateLists(){
GetVersions().then((result) => {
modpacks = result
pack = modpacks[0]
name = modpacks[0]
updateLoaders()
})
UpdateInstances()
}
function updateLoaders(){
GetFabricVersions(pack).then((result) => {
fab_versions = []
result.forEach((ver) => {
fab_versions.push(ver.Loader.Version)
})
fabric_ver = fab_versions[0]
})
GetQuiltVersions(pack).then((result) => {
quilt_versions = []
result.forEach((ver) => {
quilt_versions.push(ver.Loader.Version)
})
quilt_ver = quilt_versions[0]
})
GetForgeVersions(pack).then((result) => {
forge_versions = []
result.forEach((ver) => {
forge_versions.push(ver.Version)
})
forge_ver = forge_versions[0]
}).catch(() => { forge_versions = []; forge_ver = "" })
}
onMount(() => {
updateLists()
})
function install(){
$loading = true
InstallVanilla(pack, name).then(() => {
switch (loader){
case "none":
$addingInstance = false
$loading = false
updateLists()
break
case "fabric":
InstallFabric(name, fabric_ver).then(() => {
$addingInstance = false
$loading = false
updateLists()
})
break
case "quilt":
InstallQuilt(name, quilt_ver).then(() => {
$addingInstance = false
$loading = false
updateLists()
})
case "forge":
InstallForge(name, forge_ver).then(() => {
$addingInstance = false
$loading = false
updateLists()
})
break
}
})
}
function onchange(event){
name = event.target.value
pack = event.target.value
updateLoaders()
}
</script>
<main>
<div class=header>New Instance</div>
<br/>
<div class=container>
<div class=version-container>
{#each modpacks as modpack}
<div class="input-container" id=input-container>
<input id={modpack} type="radio" bind:group={pack} on:change={onchange} name="radio" value={modpack}>
<div class="radio-tile">
<!--icon goes here later-->
<label for={modpack}>{modpack}</label>
</div>
</div>
{/each}
</div>
<div class=loader-options>
<input type="radio" bind:group={loader} checked id="noLoader" name="Loader" value="none" />
<label for="noLoader">None</label>
<input type="radio" bind:group={loader} id="fabric" name="Loader" value="fabric" />
<label for="fabric">Fabric</label>
<input type="radio" bind:group={loader} id="forge" name="Loader" value="forge" />
<label for="forge">Forge</label>
<input type="radio" bind:group={loader} id="neoforge" name="Loader" value="neoforge" />
<label for="neoforge">NeoForge</label>
<input type="radio" bind:group={loader} id="quilt" name="Loader" value="quilt" />
<label for="quilt">Quilt</label>
<br/>
<input bind:value={name} />
{#if loader == "fabric"}
<select id="fabric_ver" bind:value={fabric_ver} name="fabric_ver">Select Fabric Version:
{#each fab_versions as ver}
<option value={ver}>{ver}</option>
{/each}
</select>
{:else if loader == "quilt"}
<select id="quilt_ver" bind:value={quilt_ver} name="quilt_ver">Select Quilt Version:
{#each quilt_versions as ver}
<option value={ver}>{ver}</option>
{/each}
</select>
{:else if loader == "forge"}
<select id="forge_ver" bind:value={forge_ver} name="forge_ver">Select Forge Version:
{#each forge_versions as ver}
<option value={ver}>{ver}</option>
{/each}
</select>
{/if}
</div>
</div>
<div transition:slide="{{duration:300}}">
<br/>
<button on:click={install}>Install</button>
<button on:click={() => {$addingInstance = false}}>Cancel</button>
</div>
</main>
<style>
main{
margin-top: 1rem;
margin-left: 3rem;
display: flex;
flex-direction: column;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-secondary: #2c2c33;
--bg-primary: #141418;
}
.container{
display: flex;
flex-direction: row;
justify-content: center;
max-height: 100%;
}
.version-container{
display: flex;
flex-direction: column;
max-height: 30rem;
flex-shrink: 0;
min-height: 0;
max-width: 20rem;
overflow-y: scroll;
scrollbar-color: var(--text-secondary) var(--bg-secondary);
overflow-x: hidden;
}
.input-container{
display: flex;
}
#pack{
display: flex;
max-width: 10rem;
}
</style>

View File

@ -0,0 +1,29 @@
<script lang="ts">
import {EventsOn} from '../wailsjs/runtime/runtime'
var stat: string = ""
var completed: number = 0
var total: number = 0
var downloading: boolean = false
EventsOn("status", (status) => {
stat = status;
})
EventsOn("download", (Completed, Total) => {
completed = (Completed / (1024*1024)).toFixed(2)
total = (Total / (1024*1024)).toFixed(2)
downloading = true
})
EventsOn("download_complete", () => {
downloading = false
})
</script>
<main>
<p id="status">{stat}</p>
{#if downloading}
<p id="download_status">{completed}MB / {total}MB</p>
{/if}
</main>

View File

@ -0,0 +1,41 @@
<script lang="ts">
import {onMount} from 'svelte'
import {GetModpacks} from '../wailsjs/go/main/ModpackManager.js'
import {ImportModpack} from '../wailsjs/go/main/InstanceManager.js'
import { main } from '../wailsjs/go/models';
import {loading} from './global.js'
let modpacks: main.Modpack[] = []
let pack: main.Modpack
export let UpdateInstances
let name: string = "New Modpack"
onMount(() => {
GetModpacks().then((result) => {
modpacks = result
pack = result[0]
name = pack.Name
})
})
function AddModpack(){
$loading = true
ImportModpack(pack, name).then(() => {
UpdateInstances()
})
}
function onchange(event){
name = pack.Name
}
</script>
<main>
<select id="pack" bind:value={pack} on:change={onchange} name="pack">Select a Modpack:
{#each modpacks as pack}
<option value={pack}>{pack.Name}</option>
{/each}
</select>
<input bind:value={name} />
<button on:click={AddModpack}>Add Modpack</button>
</main>

Some files were not shown because too many files have changed in this diff Show More