I think it's time to init a repo already. xD
This commit is contained in:
commit
d60493e1b1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
bin/
|
32
Makefile
Executable file
32
Makefile
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
TARGET=mccl
|
||||||
|
|
||||||
|
ifeq ($(shell go env GOOS),windows)
|
||||||
|
EXEC_NAME=${TARGET}.exe
|
||||||
|
else
|
||||||
|
EXEC_NAME=${TARGET}
|
||||||
|
endif
|
||||||
|
DESTDIR:=
|
||||||
|
PREFIX:=/usr/local
|
||||||
|
|
||||||
|
VERSION=0.1.0
|
||||||
|
|
||||||
|
FLAGS:=-buildmode=pie -modcacherw -mod=readonly -trimpath
|
||||||
|
LDFLAGS:= -ldflags "-s -w -X main.programVersion=${VERSION}"
|
||||||
|
|
||||||
|
.PHONY: ${TARGET} install uninstall clean
|
||||||
|
|
||||||
|
${TARGET}:
|
||||||
|
ifeq ($(shell go env GOOS),windows)
|
||||||
|
go build -o bin/$@.exe ${LDFLAGS} ${FLAGS} cmd/$@/main.go
|
||||||
|
else
|
||||||
|
go build -o bin/$@ ${LDFLAGS} ${FLAGS} cmd/$@/main.go
|
||||||
|
endif
|
||||||
|
|
||||||
|
install:
|
||||||
|
install -Dm 0755 bin/${EXEC_NAME} ${DESTDIR}${PREFIX}/bin/${EXEC_NAME}
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm ${DESTDIR}${PREFIX}/bin/${EXEC_NAME}
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f bin/*
|
26
build/PKGBUILD
Normal file
26
build/PKGBUILD
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
||||||
|
pkgname=mccl
|
||||||
|
pkgver=0.1.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Console Minecraft launcher"
|
||||||
|
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
||||||
|
url="https://git.arav.su/Arav/mccl"
|
||||||
|
license=('GPL3')
|
||||||
|
makedepends=('go>=1.17')
|
||||||
|
source=("${pkgver}.tar.gz::https://git.arav.su/Arav/mccl/archive/v${pkgver}.tar.gz")
|
||||||
|
md5sums=('SKIP')
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$srcdir/$pkgname"
|
||||||
|
export GOPATH="$srcdir"/gopath
|
||||||
|
export CGO_CPPFLAGS="${CPPFLAGS}"
|
||||||
|
export CGO_CFLAGS="${CFLAGS}"
|
||||||
|
export CGO_CXXFLAGS="${CXXFLAGS}"
|
||||||
|
export CGO_LDFLAGS="${LDFLAGS}"
|
||||||
|
make VERSION=$pkgver DESTDIR="$pkgdir" PREFIX="/usr"
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$srcdir/$pkgname"
|
||||||
|
make DESTDIR="$pkgdir" PREFIX="/usr" install
|
||||||
|
}
|
5
cmd/mccl/commands/command.go
Executable file
5
cmd/mccl/commands/command.go
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
type Command interface {
|
||||||
|
Run() error
|
||||||
|
}
|
325
cmd/mccl/commands/install_command.go
Executable file
325
cmd/mccl/commands/install_command.go
Executable file
@ -0,0 +1,325 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mccl/internal/assets"
|
||||||
|
"mccl/internal/manifest"
|
||||||
|
"mccl/internal/version_manifest"
|
||||||
|
"mccl/pkg/retriever"
|
||||||
|
"mccl/pkg/util"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstallCommand struct {
|
||||||
|
Id string
|
||||||
|
DestDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInstallCommand(id, destDir string) *InstallCommand {
|
||||||
|
return &InstallCommand{Id: id, DestDir: destDir}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *InstallCommand) Run() error {
|
||||||
|
if ic.Id == "" {
|
||||||
|
return errors.New("an empty Minecraft version was provided")
|
||||||
|
}
|
||||||
|
if ic.DestDir == "" {
|
||||||
|
return errors.New("an empty path was provided")
|
||||||
|
}
|
||||||
|
return ic.install_vanilla_client()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *InstallCommand) install_vanilla_client() error {
|
||||||
|
var err error
|
||||||
|
var data []byte
|
||||||
|
|
||||||
|
if ic.DestDir == "." {
|
||||||
|
execPath, _ := os.Executable()
|
||||||
|
ic.DestDir = path.Dir(execPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(ic.DestDir); err != nil {
|
||||||
|
if err := os.Mkdir(ic.DestDir, 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Retrieving a version_manifest_v2.json... ")
|
||||||
|
data, err = util.GetFromUrl(version_manifest.VersionManifestUrl, "", "", -1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
versionManifest, err := version_manifest.New(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
|
||||||
|
var version version_manifest.Version
|
||||||
|
if ic.Id == "release" || ic.Id == "latest-release" {
|
||||||
|
version, err = versionManifest.GetLatest(version_manifest.VersionTypeRelease)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if ic.Id == "snapshot" || ic.Id == "latest-snapshot" {
|
||||||
|
version, err = versionManifest.GetLatest(version_manifest.VersionTypeSnapshot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
version, err = versionManifest.Get(ic.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("Version %s (%s) was chosen.\n", version.Id, version.Type)
|
||||||
|
|
||||||
|
fmt.Print("Retrieving a manifest file... ")
|
||||||
|
manifestPath := path.Join(ic.DestDir, "versions", version.Id, version.Id+".json")
|
||||||
|
data, err = util.LoadOrDownloadFile(manifestPath, version.Url, version.Sha1, "sha1", -1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifst, err := manifest.New(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
|
||||||
|
if !util.IsFileExist(manifestPath) {
|
||||||
|
fmt.Print("Writing a manifest file to ", manifestPath, " ... ")
|
||||||
|
if err := util.WriteFile(manifestPath, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
} else {
|
||||||
|
fmt.Println(manifestPath, "does exist. Skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientPath := path.Join(ic.DestDir, "versions", manifst.Id, manifst.Id+".jar")
|
||||||
|
if !util.IsFileExist(clientPath) {
|
||||||
|
fmt.Print("Retrieving and writing a client to ", clientPath, " ... ")
|
||||||
|
data, err = util.LoadOrDownloadFile(clientPath, manifst.Downloads["client"].Url,
|
||||||
|
manifst.Downloads["client"].Sha1, "sha1", manifst.Downloads["client"].Size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := util.WriteFile(clientPath, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
} else {
|
||||||
|
fmt.Println(clientPath, "does exist. Skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Retrieving an asset index %s.json... ", manifst.AssetIndex.Id)
|
||||||
|
assetIndexPath := path.Join(ic.DestDir, "assets", "indexes", manifst.AssetIndex.Id+".json")
|
||||||
|
|
||||||
|
data, err = util.LoadOrDownloadFile(assetIndexPath, manifst.AssetIndex.Url,
|
||||||
|
manifst.AssetIndex.Sha1, "sha1", manifst.AssetIndex.Size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
asts, err := assets.New(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
|
||||||
|
if !util.IsFileExist(assetIndexPath) {
|
||||||
|
fmt.Printf("Writing an asset index %s to %s...", manifst.AssetIndex.Id, assetIndexPath)
|
||||||
|
if err := util.WriteFile(assetIndexPath, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s does exist. Skip writing\n", assetIndexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Retrieving assets...")
|
||||||
|
|
||||||
|
assetsDir := path.Join(ic.DestDir, "assets", "objects")
|
||||||
|
ar := retriever.New()
|
||||||
|
defer ar.Close()
|
||||||
|
|
||||||
|
assetsCount := len(asts.Objects)
|
||||||
|
var assetItems []retriever.Retrievable
|
||||||
|
|
||||||
|
for _, v := range asts.Objects {
|
||||||
|
assetItems = append(assetItems, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := ar.Run(assetItems, assets.RetrieveAsset, assetsDir); err != nil {
|
||||||
|
fmt.Println("\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for written, cnt := 0, 0; cnt < assetsCount; {
|
||||||
|
if len(ar.GetErrorChan()) > 0 {
|
||||||
|
return <-ar.GetErrorChan()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cnt < assetsCount {
|
||||||
|
written += <-ar.GetDoneChan()
|
||||||
|
}
|
||||||
|
cnt++
|
||||||
|
|
||||||
|
fmt.Printf("\rRetrieving assets... %d/%d (%d/%d)", written, manifst.AssetIndex.TotalSize, cnt, assetsCount)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nRetrieving assets... Done\n")
|
||||||
|
|
||||||
|
if manifst.Logging.Client.Argument != "" {
|
||||||
|
logConfPath := path.Join(ic.DestDir, "assets", "log_configs", manifst.Logging.Client.File.Id)
|
||||||
|
if !util.IsFileExist(logConfPath) {
|
||||||
|
fmt.Printf("Retrieving and writing %s...", logConfPath)
|
||||||
|
data, err = util.LoadOrDownloadFile(logConfPath, manifst.Logging.Client.File.Url,
|
||||||
|
manifst.Logging.Client.File.Sha1, "sha1", manifst.Logging.Client.File.Size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := util.WriteFile(logConfPath, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s does exist. Skip writing\n", logConfPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Retrieving libraries...")
|
||||||
|
|
||||||
|
librariesDir := path.Join(ic.DestDir, "libraries")
|
||||||
|
lr := retriever.New()
|
||||||
|
defer lr.Close()
|
||||||
|
|
||||||
|
var libraryItems []retriever.Retrievable
|
||||||
|
var totalLibrariesSize int64 = 0
|
||||||
|
|
||||||
|
for _, lib := range manifst.Libraries {
|
||||||
|
if lib.CheckRules() {
|
||||||
|
files := lib.ToFile()
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
totalLibrariesSize += f.Size
|
||||||
|
libraryItems = append(libraryItems, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := lr.Run(libraryItems, manifest.RetrieveLibrary, librariesDir); err != nil {
|
||||||
|
fmt.Println("\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
librariesCount := len(libraryItems)
|
||||||
|
|
||||||
|
for written, cnt := 0, 0; cnt < librariesCount; {
|
||||||
|
if len(lr.GetErrorChan()) > 0 {
|
||||||
|
return <-lr.GetErrorChan()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cnt < librariesCount {
|
||||||
|
written += <-lr.GetDoneChan()
|
||||||
|
}
|
||||||
|
cnt++
|
||||||
|
|
||||||
|
fmt.Printf("\rRetrieving libraries... %d/%d (%d/%d)",
|
||||||
|
written, totalLibrariesSize, cnt, librariesCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nRetrieving libraries... Done\n")
|
||||||
|
|
||||||
|
releaseTime, err := time.Parse(time.RFC3339, manifst.ReleaseTime)
|
||||||
|
if err != nil {
|
||||||
|
releaseTime, err = time.Parse("2006-01-02T15:04:05Z0700", manifst.ReleaseTime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstVerWithNoExtract, _ := time.Parse(time.DateOnly, "2017-10-25")
|
||||||
|
if releaseTime.Before(firstVerWithNoExtract) {
|
||||||
|
nativesDir := path.Join(ic.DestDir, "versions", manifst.Id, "natives")
|
||||||
|
|
||||||
|
if err := os.MkdirAll(nativesDir, 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Extracting native libraries to %s ... ", nativesDir)
|
||||||
|
for _, lib := range libraryItems {
|
||||||
|
l := lib.(util.File)
|
||||||
|
|
||||||
|
if !strings.Contains(l.GetName(), "natives") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile, err := zip.OpenReader(path.Join(librariesDir, l.Path))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer zipFile.Close()
|
||||||
|
|
||||||
|
for _, f := range zipFile.File {
|
||||||
|
if strings.HasPrefix(f.Name, "META-INF/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(f.Name, "64") && runtime.GOARCH != "amd64" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "386":
|
||||||
|
if strings.Contains(f.Name, "64") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case "amd64":
|
||||||
|
if !strings.Contains(f.Name, "64") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if util.IsFileExist(path.Join(nativesDir, f.Name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
frc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer frc.Close()
|
||||||
|
|
||||||
|
out, err := os.OpenFile(path.Join(nativesDir, f.Name), os.O_CREATE|os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
written, err := io.Copy(out, frc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to extract file %s: %s", f.Name, err)
|
||||||
|
} else if written != int64(f.UncompressedSize64) {
|
||||||
|
return fmt.Errorf("failed to extract file %s: size is %d, but %d was written",
|
||||||
|
f.Name, f.UncompressedSize64, written)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("Done")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
33
cmd/mccl/commands/list_command.go
Executable file
33
cmd/mccl/commands/list_command.go
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListCommand struct {
|
||||||
|
GameDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListCommand(gameDir string) *ListCommand {
|
||||||
|
return &ListCommand{GameDir: gameDir}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *ListCommand) Run() error {
|
||||||
|
versionsDir := path.Join(lc.GameDir, "versions")
|
||||||
|
if _, err := os.Stat(versionsDir); err != nil {
|
||||||
|
return fmt.Errorf("versions directory wasn't found inside %s", lc.GameDir)
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(path.Join(lc.GameDir, "versions"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("There are following versions installed:")
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
fmt.Println(entry.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
163
cmd/mccl/commands/run_command.go
Executable file
163
cmd/mccl/commands/run_command.go
Executable file
@ -0,0 +1,163 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mccl/internal/manifest"
|
||||||
|
mcclprofile "mccl/internal/mccl_profile"
|
||||||
|
"mccl/pkg/util"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RunCommand struct {
|
||||||
|
Id string
|
||||||
|
GameDir string
|
||||||
|
Username string
|
||||||
|
AuthUuid string
|
||||||
|
JavaXmx string
|
||||||
|
UseProfile bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunCommand(id, gameDir, user, uuid, jxmx string, useProfile bool) *RunCommand {
|
||||||
|
return &RunCommand{Id: id, GameDir: gameDir, Username: user, AuthUuid: uuid, JavaXmx: jxmx, UseProfile: useProfile}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RunCommand) Run() error {
|
||||||
|
if rc.Id == "" {
|
||||||
|
return errors.New("an empty Minecraft version was provided")
|
||||||
|
}
|
||||||
|
if err := os.Chdir(rc.GameDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rc.UseProfile {
|
||||||
|
if util.IsFileExist(path.Join(rc.GameDir, mcclprofile.ProfileFileName)) {
|
||||||
|
p, err := mcclprofile.Load(rc.GameDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rc.Username = p.Username
|
||||||
|
rc.AuthUuid = p.Uuid
|
||||||
|
rc.JavaXmx = p.JavaXmx
|
||||||
|
} else {
|
||||||
|
p := mcclprofile.Profile{
|
||||||
|
Username: rc.Username,
|
||||||
|
Uuid: rc.AuthUuid,
|
||||||
|
JavaXmx: rc.JavaXmx}
|
||||||
|
if err := p.Store(rc.GameDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rc.run_client()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RunCommand) run_client() error {
|
||||||
|
manifestPath := path.Join(rc.GameDir, "versions", rc.Id, rc.Id+".json")
|
||||||
|
|
||||||
|
data, err := util.ReadFile(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifst, err := manifest.New(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if manifst.InheritsFrom != "" {
|
||||||
|
manifestPath := path.Join(rc.GameDir, "versions", manifst.InheritsFrom, manifst.InheritsFrom+".json")
|
||||||
|
|
||||||
|
data, err := util.ReadFile(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
inherit, err := manifest.New(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manifst.InheritFrom(inherit); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := make(map[string]string)
|
||||||
|
|
||||||
|
p["auth_player_name"] = rc.Username
|
||||||
|
p["version_name"] = manifst.Id
|
||||||
|
p["game_directory"] = rc.GameDir
|
||||||
|
p["assets_root"] = "assets"
|
||||||
|
p["game_assets"] = p["assets_root"]
|
||||||
|
p["assets_index_name"] = manifst.AssetIndex.Id
|
||||||
|
p["auth_uuid"] = rc.AuthUuid
|
||||||
|
p["auth_access_token"] = "null"
|
||||||
|
p["auth_session"] = p["auth_access_token"]
|
||||||
|
p["clientid"] = manifst.Id
|
||||||
|
p["auth_xuid"] = "null"
|
||||||
|
p["user_type"] = "legacy"
|
||||||
|
p["version_type"] = manifst.Type
|
||||||
|
p["natives_directory"] = path.Join("", "versions", manifst.Id, "natives")
|
||||||
|
p["launcher_name"] = "mccl"
|
||||||
|
p["launcher_version"] = "0.1.0"
|
||||||
|
p["classpath"] = manifst.BuildClasspath("", "versions")
|
||||||
|
if manifst.Logging.Client.File.Id != "" {
|
||||||
|
p["logging_path"] = path.Join("assets", "log_configs", manifst.Logging.Client.File.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(manifst.Id, "forge") {
|
||||||
|
p["library_directory"] = "libraries"
|
||||||
|
p["classpath_separator"] = util.PathSeparator()
|
||||||
|
}
|
||||||
|
|
||||||
|
cl := manifst.BuildCommandLine(p)
|
||||||
|
|
||||||
|
if rc.JavaXmx != "" {
|
||||||
|
ok, err := regexp.MatchString("^\\d+(K|k|M|m|G|g|T|t)$", rc.JavaXmx)
|
||||||
|
if ok && err == nil {
|
||||||
|
rc.JavaXmx = "-Xmx" + rc.JavaXmx
|
||||||
|
tcl := []string{}
|
||||||
|
tcl = append(tcl, rc.JavaXmx)
|
||||||
|
cl = append(tcl, cl...)
|
||||||
|
} else if err != nil || !ok {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("An error occured while parsing Java -Xmx parameter: %s. Game's default will be used. ", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Mismatch occured while parsing Java -Xmx parameter. Game's default will be used. ")
|
||||||
|
}
|
||||||
|
fmt.Printf("Right form is <number>Kk|Mm|Gg|Tt, e.g. 4G.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
javaHome, err := util.LocateJavaHome(manifst.JavaVersion.Component, manifst.JavaVersion.MajorVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(path.Join(javaHome, "bin", "java"), cl...)
|
||||||
|
// stdout, err := cmd.StdoutPipe()
|
||||||
|
// cmd.Stderr = cmd.Stdout
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer stdout.Close()
|
||||||
|
|
||||||
|
// scanner := bufio.NewScanner(stdout)
|
||||||
|
// go func() {
|
||||||
|
// for scanner.Scan() {
|
||||||
|
// fmt.Println(scanner.Text())
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
|
||||||
|
fmt.Printf("Minecraft version %s is started with usename %s and player's UUID %s.\n",
|
||||||
|
manifst.Id, rc.Username, rc.AuthUuid)
|
||||||
|
if err := cmd.Run(); err != nil && (err.Error() != "exec: already started") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
232
cmd/mccl/main.go
Executable file
232
cmd/mccl/main.go
Executable file
@ -0,0 +1,232 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mccl/cmd/mccl/commands"
|
||||||
|
mcclprofile "mccl/internal/mccl_profile"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var programVersion string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args, err := parseArguments(os.Args[1:])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.ShowVersion {
|
||||||
|
version()
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.ShowHelp {
|
||||||
|
usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.ShowHelp || args.ShowVersion {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd commands.Command
|
||||||
|
|
||||||
|
switch args.Command {
|
||||||
|
case "install":
|
||||||
|
fallthrough
|
||||||
|
case "i":
|
||||||
|
cmd = commands.NewInstallCommand(args.GameVersion, args.Directory)
|
||||||
|
case "run":
|
||||||
|
fallthrough
|
||||||
|
case "r":
|
||||||
|
cmd = commands.NewRunCommand(args.GameVersion, args.Directory,
|
||||||
|
args.Username, args.Uuid, args.JavaXmx, args.UseProfile)
|
||||||
|
case "list":
|
||||||
|
fallthrough
|
||||||
|
case "l":
|
||||||
|
cmd = commands.NewListCommand(args.Directory)
|
||||||
|
default:
|
||||||
|
usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode := 0
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.DemandPressEnter {
|
||||||
|
fmt.Fprint(os.Stderr, "Press the enter key to continue...")
|
||||||
|
fmt.Scanln()
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func version() {
|
||||||
|
fmt.Fprintf(os.Stderr, "mccl ver. %s\nCopyright (c) 2023 Alexander \"Arav\" Andreev <me@arav.su>\n", programVersion)
|
||||||
|
fmt.Fprintln(os.Stderr, "License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage: mccl [<common options>]... command arguments... [options]...")
|
||||||
|
fmt.Fprintln(os.Stderr, "Common options:\n"+
|
||||||
|
" -h,--help show help\n"+
|
||||||
|
" -v,--version show version\n"+
|
||||||
|
" --demand-press-enter\n"+
|
||||||
|
" ask to press the enter key before closing\n"+
|
||||||
|
"Commands:\n"+
|
||||||
|
" \033[1minstall|i\033[0m version directory\n"+
|
||||||
|
" Install any vanilla version (for mod loaders use theirs installers)\n"+
|
||||||
|
" \033[1mrun|r\033[0m version directory [-u username] [-U UUID] [-java-Xmx memory] [--profile]\n"+
|
||||||
|
" Run vanilla, forge, fabric, quilt, the others aren't tested\n"+
|
||||||
|
" \033[1mlist|l\033[0m directory\n"+
|
||||||
|
" List all installed versions in directory\n"+
|
||||||
|
"Explanation of arguments and options for commands:\n"+
|
||||||
|
" version minecraft's version \n"+
|
||||||
|
" directory a path to where Minecraft will be installed\n"+
|
||||||
|
" -u,--username username\n"+
|
||||||
|
" player's username (Anonymous by default)\n"+
|
||||||
|
" -U,--uuid UUID\n"+
|
||||||
|
" player's account UUID (all zeroes by default)\n"+
|
||||||
|
" --java-Xmx memory\n"+
|
||||||
|
" java's -Xmx param, e.g. 4G\n"+
|
||||||
|
" --profile\n"+
|
||||||
|
" load/save username, UUID, and -Xmx from/to a file.\n"+
|
||||||
|
" Once saved, you can ommit -u,-U and --java-Xmx,\n"+
|
||||||
|
" since they are stored in", mcclprofile.ProfileFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
type arguments struct {
|
||||||
|
ShowVersion bool
|
||||||
|
ShowHelp bool
|
||||||
|
DemandPressEnter bool
|
||||||
|
|
||||||
|
Command string
|
||||||
|
GameVersion string
|
||||||
|
Directory string
|
||||||
|
Username string
|
||||||
|
Uuid string
|
||||||
|
JavaXmx string
|
||||||
|
|
||||||
|
UseProfile bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type errNoOptArg struct {
|
||||||
|
ArgName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errNoOptArg) Error() string {
|
||||||
|
return fmt.Sprintf("%s option demands an argument, but it wasn't provided", e.ArgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArguments(args []string) (parsed arguments, err error) {
|
||||||
|
parsed.Username = "Anonymous"
|
||||||
|
parsed.Directory = "."
|
||||||
|
parsed.Uuid = "00000000-0000-0000-0000-000000000000"
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
if len(args[i]) > 2 && args[i][0] == '-' && args[i][1] == '-' {
|
||||||
|
switch args[i][2:] {
|
||||||
|
case "version":
|
||||||
|
parsed.ShowVersion = true
|
||||||
|
case "help":
|
||||||
|
parsed.ShowHelp = true
|
||||||
|
case "demand-press-enter":
|
||||||
|
parsed.DemandPressEnter = true
|
||||||
|
case "username":
|
||||||
|
if len(args) == i+1 || args[i+1][0] == '-' {
|
||||||
|
err = errNoOptArg{ArgName: args[i]}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed.Username = args[i+1]
|
||||||
|
i++
|
||||||
|
case "uuid":
|
||||||
|
if len(args) == i+1 || args[i+1][0] == '-' {
|
||||||
|
err = errNoOptArg{ArgName: args[i]}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed.Uuid = args[i+1]
|
||||||
|
i++
|
||||||
|
case "java-Xmx":
|
||||||
|
if len(args) == i+1 || args[i+1][0] == '-' {
|
||||||
|
err = errNoOptArg{ArgName: args[i]}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed.JavaXmx = args[i+1]
|
||||||
|
i++
|
||||||
|
case "profile":
|
||||||
|
parsed.UseProfile = true
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("an unknown option %s was provided", args[i])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if args[i][0] == '-' && len(args[i]) > 1 {
|
||||||
|
for _, option := range args[i][1:] {
|
||||||
|
switch option {
|
||||||
|
case 'v':
|
||||||
|
parsed.ShowVersion = true
|
||||||
|
case 'h':
|
||||||
|
parsed.ShowHelp = true
|
||||||
|
case 'u':
|
||||||
|
if len(args) == i+1 || args[i+1][0] == '-' {
|
||||||
|
err = errNoOptArg{ArgName: args[i]}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed.Username = args[i+1]
|
||||||
|
i++
|
||||||
|
case 'U':
|
||||||
|
if len(args) == i+1 || args[i+1][0] == '-' {
|
||||||
|
err = errNoOptArg{ArgName: args[i]}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed.Uuid = args[i+1]
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("an unknown option %c was provided", option)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if parsed.Command != "" {
|
||||||
|
err = fmt.Errorf("only one command should present")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch parsed.Command = args[i]; parsed.Command {
|
||||||
|
case "i":
|
||||||
|
fallthrough
|
||||||
|
case "install":
|
||||||
|
fallthrough
|
||||||
|
case "r":
|
||||||
|
fallthrough
|
||||||
|
case "run":
|
||||||
|
if len(args) <= i+3 || args[i+1][0] == '-' || args[i+2][0] == '-' {
|
||||||
|
err = fmt.Errorf("not enough arguments was provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed.GameVersion = args[i+1]
|
||||||
|
parsed.Directory = args[i+2]
|
||||||
|
i += 2
|
||||||
|
case "l":
|
||||||
|
fallthrough
|
||||||
|
case "list":
|
||||||
|
if len(args) <= i+2 || args[i+1][0] == '-' {
|
||||||
|
err = fmt.Errorf("not enough arguments was provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed.Directory = args[i+1]
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("an unknown command %s was provided", parsed.Command)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
5
go.mod
Executable file
5
go.mod
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
module mccl
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.13.0
|
2
go.sum
Executable file
2
go.sum
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
124
internal/assets/assets.go
Executable file
124
internal/assets/assets.go
Executable file
@ -0,0 +1,124 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mccl/pkg/retriever"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// assetDownloadUrl is a base URL for an asset to be retrieved from, first %s
|
||||||
|
// is a directory which name is first two characters of a hash that goes
|
||||||
|
// under the second %s.
|
||||||
|
const assetDownloadUrl = "https://resources.download.minecraft.net/%s/%s"
|
||||||
|
|
||||||
|
// Asset represents a single asset file, that is some resource like a texture,
|
||||||
|
// or a sound, and so on.
|
||||||
|
type Asset struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// url forms a full URL to an asset.
|
||||||
|
func (a *Asset) url() string {
|
||||||
|
return fmt.Sprintf(assetDownloadUrl, a.Hash[:2], a.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// path forms a relative path to an asset.
|
||||||
|
func (a *Asset) path() string {
|
||||||
|
return path.Join(a.Hash[:2], a.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve is a method of a Retrievable interface that is used to download
|
||||||
|
// an asset from Mojang's server. It performs the checks for a valid size
|
||||||
|
// and hash. Returns a slice of bytes an asset consists of or an error
|
||||||
|
// if something gone wrong.
|
||||||
|
func (a Asset) Retrieve() ([]byte, error) {
|
||||||
|
resp, err := http.Get(a.url())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to download a file. Status code is %d", resp.StatusCode)
|
||||||
|
} else if resp.ContentLength != a.Size {
|
||||||
|
return nil, fmt.Errorf("response size mismatch. %d != %d", resp.ContentLength, a.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(buf); l != int(a.Size) {
|
||||||
|
return nil, fmt.Errorf("file size mismatch. %d != %d", l, a.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := sha1.New()
|
||||||
|
if _, err := h.Write(buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hex.EncodeToString(h.Sum(nil)) != a.Hash {
|
||||||
|
return nil, errors.New("hash mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets represents an asset index file.
|
||||||
|
type Assets struct {
|
||||||
|
Objects map[string]Asset `json:"objects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Assets that is being unmarshaled from a JSON data.
|
||||||
|
func New(data []byte) (*Assets, error) {
|
||||||
|
a := &Assets{}
|
||||||
|
if err := json.Unmarshal(data, a); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetrieveAsset is a special retriever.WorkerFunc function that is used to
|
||||||
|
// check for an existency of an asset, and, if not, to retrieve it and write
|
||||||
|
// it to an actual file.
|
||||||
|
func RetrieveAsset(item retriever.Retrievable, basePath string) (int, error) {
|
||||||
|
asset := item.(Asset)
|
||||||
|
|
||||||
|
p := path.Join(basePath, asset.path())
|
||||||
|
if s, err := os.Stat(p); err == nil && s.Size() == asset.Size {
|
||||||
|
return int(s.Size()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := item.Retrieve()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Dir(p), 0777); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tf, err := os.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer tf.Close()
|
||||||
|
|
||||||
|
written, err := tf.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if written != len(data) {
|
||||||
|
return 0, fmt.Errorf("written bytes length mismatch. %d != %d", written, len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return written, nil
|
||||||
|
}
|
60
internal/manifest/argument.go
Executable file
60
internal/manifest/argument.go
Executable file
@ -0,0 +1,60 @@
|
|||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Argument is a special struct that was made to handle a mixed-typed array
|
||||||
|
// of arguments in a manifest.
|
||||||
|
//
|
||||||
|
// An argument in a manifest could be:
|
||||||
|
// - a single string
|
||||||
|
// - an object consists of rules array field and a value field that could be
|
||||||
|
// a single string or an array of strings.
|
||||||
|
type Argument struct {
|
||||||
|
Rules []Rule `json:"rules"`
|
||||||
|
Value []string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckRules returns true if all rules passed and a feature set in a rule if
|
||||||
|
// applicable, a FeatureUnknown otherwise.
|
||||||
|
func (a Argument) CheckRules() (ok bool, feature Feature) {
|
||||||
|
feature = FeatureUnknown
|
||||||
|
for _, r := range a.Rules {
|
||||||
|
ok, feature = r.Check()
|
||||||
|
if !ok {
|
||||||
|
return false, feature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, feature
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON had to be implemented because of mixed-typed nature of an
|
||||||
|
// arguments array in a manifest.
|
||||||
|
func (a *Argument) UnmarshalJSON(data []byte) error {
|
||||||
|
if data[0] == '"' {
|
||||||
|
a.Value = append(a.Value, string(data[1:len(data)-1]))
|
||||||
|
} else if data[0] == '{' && bytes.Contains(data, []byte("value\": \"")) {
|
||||||
|
singleValArg := struct {
|
||||||
|
Rules []Rule `json:"rules"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(data, &singleValArg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Rules = singleValArg.Rules
|
||||||
|
a.Value = append(a.Value, singleValArg.Value)
|
||||||
|
} else if data[0] == '{' {
|
||||||
|
multiValArg := struct {
|
||||||
|
Rules []Rule `json:"rules"`
|
||||||
|
Value []string `json:"value"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(data, &multiValArg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Rules = multiValArg.Rules
|
||||||
|
a.Value = multiValArg.Value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
16
internal/manifest/argument_test.go
Executable file
16
internal/manifest/argument_test.go
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArgument(t *testing.T) {
|
||||||
|
s := []byte("{\"rules\": [{ \"action\": \"allow\", \"os\": { \"name\": \"osx\" } }], \"value\":[\"-XstartOnFirstThread\"]}")
|
||||||
|
a := Argument{}
|
||||||
|
if err := json.Unmarshal(s, &a); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ok, f := a.CheckRules()
|
||||||
|
t.Log(a, ok, f)
|
||||||
|
}
|
136
internal/manifest/library.go
Executable file
136
internal/manifest/library.go
Executable file
@ -0,0 +1,136 @@
|
|||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mccl/pkg/retriever"
|
||||||
|
"mccl/pkg/util"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Library struct {
|
||||||
|
Downloads struct {
|
||||||
|
Artifact util.File `json:"artifact"`
|
||||||
|
Classifiers map[string]util.File `json:"classifiers"`
|
||||||
|
} `json:"downloads"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Rules []Rule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Library) Path() string {
|
||||||
|
if l.Downloads.Artifact.Path != "" {
|
||||||
|
return l.Downloads.Artifact.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.pathFromName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Library) ToFile() []util.File {
|
||||||
|
var f []util.File
|
||||||
|
if l.Downloads.Artifact.Url != "" {
|
||||||
|
f = append(f, l.Downloads.Artifact)
|
||||||
|
} else if l.Url != "" {
|
||||||
|
lf := util.File{
|
||||||
|
Id: l.Name,
|
||||||
|
Url: path.Join(l.Url, l.pathFromName()),
|
||||||
|
Size: -1,
|
||||||
|
Path: l.pathFromName(),
|
||||||
|
}
|
||||||
|
f = append(f, lf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if natives := l.Natives(); natives != nil {
|
||||||
|
f = append(f, *natives)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Library) pathFromName() string {
|
||||||
|
parts := strings.Split(l.Name, ":")
|
||||||
|
parts = append(strings.Split(parts[0], "."), parts[1:]...)
|
||||||
|
pth := path.Join(parts...)
|
||||||
|
jarName := strings.Join([]string{parts[len(parts)-2], parts[len(parts)-1]}, "-") + ".jar"
|
||||||
|
return path.Join(pth, jarName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetrieveLibrary(item retriever.Retrievable, destDir string) (int, error) {
|
||||||
|
l := item.(util.File)
|
||||||
|
p := path.Join(destDir, l.Path)
|
||||||
|
|
||||||
|
if s, err := os.Stat(p); err == nil && s.Size() == l.Size {
|
||||||
|
return int(l.Size), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := item.Retrieve()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Dir(p), 0777); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tf, err := os.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer tf.Close()
|
||||||
|
|
||||||
|
written, err := tf.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if written != len(data) {
|
||||||
|
return 0, fmt.Errorf("written bytes length mismatch. %d != %d", written, len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return written, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Library) CheckRules() bool {
|
||||||
|
if strings.Contains(l.Name, "natives") {
|
||||||
|
nat := strings.SplitAfter(l.Name, ":")[3]
|
||||||
|
nats := strings.Split(nat, "-")
|
||||||
|
|
||||||
|
archR := Rule{Action: ActionAllow}
|
||||||
|
if len(nats) == 3 {
|
||||||
|
archR.Os.Arch = nats[2]
|
||||||
|
l.Rules = append(l.Rules, archR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range l.Rules {
|
||||||
|
if ok, _ := r.Check(); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Library) Natives() *util.File {
|
||||||
|
if !l.CheckRules() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var nativeOs string
|
||||||
|
switch v := runtime.GOOS; v {
|
||||||
|
case "windows":
|
||||||
|
fallthrough
|
||||||
|
case "linux":
|
||||||
|
nativeOs = "natives-" + v
|
||||||
|
case "darwin":
|
||||||
|
nativeOs = "natives-macos"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(l.Downloads.Classifiers) > 0 {
|
||||||
|
if v, ok := l.Downloads.Classifiers[nativeOs]; ok {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
200
internal/manifest/manifest.go
Executable file
200
internal/manifest/manifest.go
Executable file
@ -0,0 +1,200 @@
|
|||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mccl/pkg/util"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manifest struct {
|
||||||
|
InheritsFrom string `json:"inheritsFrom"`
|
||||||
|
Arguments struct {
|
||||||
|
Game []Argument `json:"game"`
|
||||||
|
Jvm []Argument `json:"jvm"`
|
||||||
|
} `json:"arguments"`
|
||||||
|
AssetIndex struct {
|
||||||
|
util.File
|
||||||
|
TotalSize int `json:"totalSize"`
|
||||||
|
} `json:"assetIndex"`
|
||||||
|
Assets string `json:"assets"`
|
||||||
|
ComplianceLevel int `json:"complianceLevel"`
|
||||||
|
Downloads map[string]util.File `json:"downloads"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
JavaVersion struct {
|
||||||
|
Component string `json:"component"`
|
||||||
|
MajorVersion int `json:"majorVersion"`
|
||||||
|
} `json:"javaVersion"`
|
||||||
|
Libraries []Library `json:"libraries"`
|
||||||
|
Logging struct {
|
||||||
|
Client struct {
|
||||||
|
Argument string `json:"argument"`
|
||||||
|
File util.File `json:"file"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"client"`
|
||||||
|
} `json:"logging"`
|
||||||
|
MainClass string `json:"mainClass"`
|
||||||
|
MinecraftArguments string `json:"minecraftArguments"`
|
||||||
|
MinimumLauncherVersion int `json:"minimumLauncherVersion"`
|
||||||
|
ReleaseTime string `json:"releaseTime"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Manifest for a specific Minecraft version.
|
||||||
|
func New(data []byte) (*Manifest, error) {
|
||||||
|
m := &Manifest{}
|
||||||
|
if err := json.Unmarshal(data, m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manifest) BuildCommandLine(properties map[string]string) (arguments []string) {
|
||||||
|
if m.MinecraftArguments != "" {
|
||||||
|
arguments = append(arguments, fmt.Sprintf("-Djava.library.path=%s", properties["natives_directory"]))
|
||||||
|
arguments = append(arguments, "-cp")
|
||||||
|
arguments = append(arguments, properties["classpath"])
|
||||||
|
arguments = append(arguments, m.MainClass)
|
||||||
|
args := strings.Split(m.MinecraftArguments, " ")
|
||||||
|
for _, arg := range args {
|
||||||
|
if strings.HasPrefix(arg, "${") {
|
||||||
|
propName := arg[2 : len(arg)-1]
|
||||||
|
arg = properties[propName]
|
||||||
|
}
|
||||||
|
arguments = append(arguments, arg)
|
||||||
|
}
|
||||||
|
return arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
var allArgs []Argument
|
||||||
|
|
||||||
|
allArgs = append(allArgs, m.Arguments.Jvm...)
|
||||||
|
|
||||||
|
if m.Logging.Client.Argument != "" {
|
||||||
|
logConf := strings.Split(m.Logging.Client.Argument, "${")[0]
|
||||||
|
logConf += properties["logging_path"]
|
||||||
|
allArgs = append(allArgs, Argument{Value: []string{logConf}})
|
||||||
|
}
|
||||||
|
|
||||||
|
allArgs = append(allArgs, Argument{Value: []string{m.MainClass}})
|
||||||
|
allArgs = append(allArgs, m.Arguments.Game...)
|
||||||
|
|
||||||
|
for _, arg := range allArgs {
|
||||||
|
ok, feature := arg.CheckRules()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
} else if feature != FeatureUnknown {
|
||||||
|
if _, ok := properties[feature.String()]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range arg.Value {
|
||||||
|
if strings.Contains(v, "${library_directory}") {
|
||||||
|
v = strings.ReplaceAll(v, "${library_directory}", properties["library_directory"])
|
||||||
|
v = strings.ReplaceAll(v, "${classpath_separator}", properties["classpath_separator"])
|
||||||
|
} else if i := strings.Index(v, "${"); i != -1 {
|
||||||
|
j := strings.Index(v, "}")
|
||||||
|
propName := v[i+2 : j]
|
||||||
|
switch propName {
|
||||||
|
case "version_name":
|
||||||
|
if strings.Contains(m.Id, "forge") {
|
||||||
|
v = strings.ReplaceAll(v, "${version_name}", m.InheritsFrom)
|
||||||
|
} else {
|
||||||
|
v = strings.ReplaceAll(v, "${version_name}", m.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "assets_index_name":
|
||||||
|
v = m.Assets
|
||||||
|
case "version_type":
|
||||||
|
v = m.Type
|
||||||
|
default:
|
||||||
|
v = strings.Replace(v, v[i:j+1], properties[propName], 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arguments = append(arguments, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manifest) BuildClasspath(baseDir, clientPath string) string {
|
||||||
|
var sb []string = make([]string, 0, len(m.Libraries))
|
||||||
|
for _, lib := range m.Libraries {
|
||||||
|
if lib.CheckRules() {
|
||||||
|
sb = append(sb, path.Join(baseDir, "libraries", lib.Path()))
|
||||||
|
if natives := lib.Natives(); natives != nil {
|
||||||
|
sb = append(sb, path.Join(baseDir, "libraries", natives.Path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.InheritsFrom != "" {
|
||||||
|
sb = append(sb, path.Join(clientPath, m.InheritsFrom, m.InheritsFrom+".jar"))
|
||||||
|
} else {
|
||||||
|
sb = append(sb, path.Join(clientPath, m.Id, m.Id+".jar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(sb, util.PathSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manifest) InheritFrom(parent *Manifest) error {
|
||||||
|
if parent == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Arguments.Game = append(parent.Arguments.Game, m.Arguments.Game...)
|
||||||
|
m.Arguments.Jvm = append(parent.Arguments.Jvm, m.Arguments.Jvm...)
|
||||||
|
|
||||||
|
if m.AssetIndex.TotalSize == 0 {
|
||||||
|
m.AssetIndex.File = parent.AssetIndex.File
|
||||||
|
m.AssetIndex.TotalSize = parent.AssetIndex.TotalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Assets == "" {
|
||||||
|
m.Assets = parent.Assets
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ComplianceLevel == 0 {
|
||||||
|
m.ComplianceLevel = parent.ComplianceLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Downloads == nil {
|
||||||
|
m.Downloads = make(map[string]util.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range parent.Downloads {
|
||||||
|
m.Downloads[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Id == "" {
|
||||||
|
m.Id = parent.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.JavaVersion.Component != "" {
|
||||||
|
m.JavaVersion.Component = parent.JavaVersion.Component
|
||||||
|
m.JavaVersion.MajorVersion = parent.JavaVersion.MajorVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Libraries = append(m.Libraries, parent.Libraries...)
|
||||||
|
|
||||||
|
if m.Logging.Client.Argument == "" {
|
||||||
|
m.Logging.Client.Argument = parent.Logging.Client.Argument
|
||||||
|
m.Logging.Client.Type = parent.Logging.Client.Type
|
||||||
|
m.Logging.Client.File = parent.Logging.Client.File
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.MainClass == "" {
|
||||||
|
m.MainClass = parent.MainClass
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.MinecraftArguments == "" {
|
||||||
|
m.MinecraftArguments = parent.MinecraftArguments
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
122
internal/manifest/rule.go
Executable file
122
internal/manifest/rule.go
Executable file
@ -0,0 +1,122 @@
|
|||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mccl/pkg/util"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Feature int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FeatureIsDemoUser Feature = iota
|
||||||
|
FeatureHasCustomResolution
|
||||||
|
FeatureHasQuickPlaysSupport
|
||||||
|
FeatureIsQuickPlaySingleplayer
|
||||||
|
FeatureIsQuickPlayMultiplayer
|
||||||
|
FeatureIsQuickPlayRealms
|
||||||
|
FeatureUnknown
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f Feature) String() string {
|
||||||
|
switch f {
|
||||||
|
case FeatureIsDemoUser:
|
||||||
|
return "is_demo_user"
|
||||||
|
case FeatureHasCustomResolution:
|
||||||
|
return "has_custom_resolution"
|
||||||
|
case FeatureHasQuickPlaysSupport:
|
||||||
|
return "has_quick_plays_support"
|
||||||
|
case FeatureIsQuickPlayMultiplayer:
|
||||||
|
return "is_quick_play_multiplayer"
|
||||||
|
case FeatureIsQuickPlayRealms:
|
||||||
|
return "is_quick_play_realms"
|
||||||
|
case FeatureIsQuickPlaySingleplayer:
|
||||||
|
return "is_quick_play_singleplayer"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionAllow Action = "allow"
|
||||||
|
ActionDisallow Action = "disallow"
|
||||||
|
ActionUnknown Action = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Action Action `json:"action"`
|
||||||
|
Os struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
} `json:"os"`
|
||||||
|
Features struct {
|
||||||
|
IsDemoUser bool `json:"is_demo_user"`
|
||||||
|
HasCustomResolution bool `json:"has_custom_resolution"`
|
||||||
|
HasQuickPlaysSupport bool `json:"has_quick_plays_support"`
|
||||||
|
IsQuickPlaySingleplayer bool `json:"is_quick_play_singleplayer"`
|
||||||
|
IsQuickPlayMultiplayer bool `json:"is_quick_play_multiplayer"`
|
||||||
|
IsQuickPlayRealms bool `json:"is_quick_play_realms"`
|
||||||
|
} `json:"features"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) GetFeature() Feature {
|
||||||
|
switch {
|
||||||
|
case r.Features.IsDemoUser:
|
||||||
|
return FeatureIsDemoUser
|
||||||
|
case r.Features.HasCustomResolution:
|
||||||
|
return FeatureHasCustomResolution
|
||||||
|
case r.Features.HasQuickPlaysSupport:
|
||||||
|
return FeatureHasQuickPlaysSupport
|
||||||
|
case r.Features.IsQuickPlayMultiplayer:
|
||||||
|
return FeatureIsQuickPlayMultiplayer
|
||||||
|
case r.Features.IsQuickPlaySingleplayer:
|
||||||
|
return FeatureIsQuickPlaySingleplayer
|
||||||
|
case r.Features.IsQuickPlayRealms:
|
||||||
|
return FeatureIsQuickPlayRealms
|
||||||
|
default:
|
||||||
|
return FeatureUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) Check() (bool, Feature) {
|
||||||
|
feature := r.GetFeature()
|
||||||
|
|
||||||
|
osArchOk := true
|
||||||
|
switch r.Os.Arch {
|
||||||
|
case "x86":
|
||||||
|
osArchOk = r.Action == ActionAllow && runtime.GOARCH == "386"
|
||||||
|
case "x86_64":
|
||||||
|
osArchOk = r.Action == ActionAllow && runtime.GOARCH == "amd64"
|
||||||
|
case "arm64":
|
||||||
|
osArchOk = r.Action == ActionAllow && runtime.GOARCH == "arm64"
|
||||||
|
}
|
||||||
|
|
||||||
|
osName := runtime.GOOS
|
||||||
|
if osName == "darwin" {
|
||||||
|
osName = "osx"
|
||||||
|
}
|
||||||
|
|
||||||
|
osNameOk := true
|
||||||
|
if r.Os.Name != "" {
|
||||||
|
if r.Action == ActionAllow {
|
||||||
|
osNameOk = r.Os.Name == osName
|
||||||
|
} else {
|
||||||
|
osNameOk = r.Os.Name != osName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
osVersionOk := true
|
||||||
|
if r.Os.Name == osName && r.Os.Version != "" {
|
||||||
|
ver, err := util.OSVersion()
|
||||||
|
if err != nil {
|
||||||
|
return false, feature
|
||||||
|
}
|
||||||
|
ok, _ := regexp.MatchString(r.Os.Version, ver)
|
||||||
|
osVersionOk = ok && r.Action == ActionAllow
|
||||||
|
}
|
||||||
|
|
||||||
|
return osArchOk && osNameOk && osVersionOk, feature
|
||||||
|
}
|
49
internal/manifest/rule_test.go
Executable file
49
internal/manifest/rule_test.go
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
package manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRuleOsName(t *testing.T) {
|
||||||
|
macos := Rule{Action: ActionAllow}
|
||||||
|
macos.Os.Name = "osx"
|
||||||
|
|
||||||
|
if ok, _ := macos.Check(); runtime.GOOS != "darwin" && ok {
|
||||||
|
t.Fatal("Rule.Os.Name == 'osx', but is true on ", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
linux := Rule{Action: ActionAllow}
|
||||||
|
linux.Os.Name = "linux"
|
||||||
|
|
||||||
|
if ok, _ := linux.Check(); runtime.GOOS != "linux" && ok {
|
||||||
|
t.Fatalf("Rule.Os.Name == '%s', but is true on %s\n", linux.Os.Name, runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
windows := Rule{Action: ActionAllow}
|
||||||
|
windows.Os.Name = "windows"
|
||||||
|
|
||||||
|
if ok, _ := windows.Check(); runtime.GOOS != "windows" && ok {
|
||||||
|
t.Fatalf("Rule.Os.Name == '%s', but is true on %s\n", windows.Os.Name, runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
x86 := Rule{Action: ActionAllow}
|
||||||
|
x86.Os.Arch = "x86"
|
||||||
|
|
||||||
|
if ok, _ := x86.Check(); runtime.GOARCH != "386" && ok {
|
||||||
|
t.Fatalf("Rule.Os.Arch == '%s', but is true on %s\n", windows.Os.Arch, runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyAllow := Rule{Action: ActionAllow}
|
||||||
|
|
||||||
|
if ok, _ := emptyAllow.Check(); !ok {
|
||||||
|
t.Fatalf("An empty allow rule must return true\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
disOsx := Rule{Action: ActionDisallow}
|
||||||
|
disOsx.Os.Name = "osx"
|
||||||
|
|
||||||
|
if ok, _ := disOsx.Check(); runtime.GOOS != "darwin" && !ok {
|
||||||
|
t.Fatalf("A disallow rule for OSX must return false, and true for other OSes\n")
|
||||||
|
}
|
||||||
|
}
|
47
internal/mccl_profile/profile.go
Normal file
47
internal/mccl_profile/profile.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package mcclprofile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ProfileFileName = "mccl_profile.json"
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
JavaXmx string `json:"java-Xmx"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Store(rootDir string) error {
|
||||||
|
pf, err := os.OpenFile(path.Join(rootDir, ProfileFileName), os.O_CREATE|os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer pf.Close()
|
||||||
|
|
||||||
|
je := json.NewEncoder(pf)
|
||||||
|
je.SetIndent("", "\t")
|
||||||
|
if err := je.Encode(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(rootDir string) (*Profile, error) {
|
||||||
|
p := &Profile{}
|
||||||
|
pf, err := os.Open(path.Join(rootDir, ProfileFileName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer pf.Close()
|
||||||
|
|
||||||
|
jd := json.NewDecoder(pf)
|
||||||
|
if err := jd.Decode(p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
74
internal/version_manifest/version_manifest.go
Executable file
74
internal/version_manifest/version_manifest.go
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
package version_manifest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VersionManifestUrl is a URL to a version manifest JSON file of version 2.
|
||||||
|
const VersionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
|
||||||
|
|
||||||
|
// VersionType is telling if it is a stable release or an intermedialry snapshot.
|
||||||
|
type VersionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VersionTypeSnapshot is an unstable intermediary version of a game.
|
||||||
|
VersionTypeSnapshot VersionType = "snapshot"
|
||||||
|
// VersionTypeRelease is a stable release version of a game.
|
||||||
|
VersionTypeRelease VersionType = "release"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version holds an information about a particular version.
|
||||||
|
type Version struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type VersionType `json:"type"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
ReleaseTime time.Time `json:"releaseTime"`
|
||||||
|
Sha1 string `json:"sha1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionManifest holds an array of all game versions, also a latest metadata
|
||||||
|
// that holds the latest release and snapshot.
|
||||||
|
type VersionManifest struct {
|
||||||
|
Latest struct {
|
||||||
|
Release string `json:"release"`
|
||||||
|
Snapshot string `json:"snapshot"`
|
||||||
|
} `json:"latest"`
|
||||||
|
Versions []Version `json:"versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new VersionManifest that holds info on every
|
||||||
|
// known Minecraft version.
|
||||||
|
func New(data []byte) (*VersionManifest, error) {
|
||||||
|
vm := &VersionManifest{}
|
||||||
|
if err := json.Unmarshal(data, vm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return vm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatest returns a latest release or snapshot depending on a type provided.
|
||||||
|
func (vm *VersionManifest) GetLatest(typ VersionType) (Version, error) {
|
||||||
|
var version string
|
||||||
|
switch typ {
|
||||||
|
case VersionTypeSnapshot:
|
||||||
|
version = vm.Latest.Snapshot
|
||||||
|
case VersionTypeRelease:
|
||||||
|
version = vm.Latest.Release
|
||||||
|
default:
|
||||||
|
return Version{}, fmt.Errorf("version type %s doesn't exist", typ)
|
||||||
|
}
|
||||||
|
return vm.Get(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a specific version of Minecraft.
|
||||||
|
func (vm *VersionManifest) Get(version string) (Version, error) {
|
||||||
|
for _, v := range vm.Versions {
|
||||||
|
if v.Id == version {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Version{}, fmt.Errorf("version %s not found", version)
|
||||||
|
}
|
81
pkg/retriever/retriever.go
Executable file
81
pkg/retriever/retriever.go
Executable file
@ -0,0 +1,81 @@
|
|||||||
|
package retriever
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Retrievable must be implemented by any type used with Retriever.
|
||||||
|
type Retrievable interface {
|
||||||
|
// Retrieve is used just to retrieve some data from somewhere (usually from
|
||||||
|
// network) and return a slice of bytes or nil and an error. If you need to
|
||||||
|
// write the data retrieved use a WorkerFunc where you can call this method.
|
||||||
|
Retrieve() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retriever allows to retrieve a bunch of files in parallel taking care of
|
||||||
|
// errors and a done channel that returns an amount of bytes written.
|
||||||
|
type Retriever struct {
|
||||||
|
retrieveErrors chan error
|
||||||
|
retrieveDone chan int
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Retriever with a base path set.
|
||||||
|
func New() *Retriever {
|
||||||
|
return &Retriever{
|
||||||
|
retrieveErrors: make(chan error, 1),
|
||||||
|
retrieveDone: make(chan int)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorChan returns an error channel.
|
||||||
|
//
|
||||||
|
// An error channel holds just one error and hangs after that. So it is considered
|
||||||
|
// as a "soft panic".
|
||||||
|
func (r *Retriever) GetErrorChan() chan error {
|
||||||
|
return r.retrieveErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDoneChan returns a channel that is used to track a progress. Its value
|
||||||
|
// is an amount of bytes written for an item.
|
||||||
|
func (r *Retriever) GetDoneChan() chan int {
|
||||||
|
return r.retrieveDone
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkerFunc accepts a Retrievable item and a basePath where an item should be
|
||||||
|
// placed to. And it returns how much was written and an error if something
|
||||||
|
// went wrong.
|
||||||
|
type WorkerFunc func(item Retrievable, basePath string) (int, error)
|
||||||
|
|
||||||
|
// Run runs in parallel a number of CPU threads * 2 WorkerFunc functions.
|
||||||
|
// If an error occured it stops and returns that error.
|
||||||
|
func (r *Retriever) Run(items []Retrievable, worker WorkerFunc, basePath string) error {
|
||||||
|
var workersGroup sync.WaitGroup
|
||||||
|
retrieveLimiter := make(chan struct{}, runtime.NumCPU()*2)
|
||||||
|
for _, item := range items {
|
||||||
|
if len(r.retrieveErrors) > 0 {
|
||||||
|
workersGroup.Wait()
|
||||||
|
return <-r.retrieveErrors
|
||||||
|
}
|
||||||
|
retrieveLimiter <- struct{}{}
|
||||||
|
workersGroup.Add(1)
|
||||||
|
go func(item Retrievable) {
|
||||||
|
bytesLen, err := worker(item, basePath)
|
||||||
|
workersGroup.Done()
|
||||||
|
<-retrieveLimiter
|
||||||
|
if err != nil {
|
||||||
|
r.retrieveErrors <- fmt.Errorf("failed to retrieve: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.retrieveDone <- bytesLen
|
||||||
|
}(item)
|
||||||
|
}
|
||||||
|
workersGroup.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the channels.
|
||||||
|
func (r *Retriever) Close() {
|
||||||
|
close(r.retrieveDone)
|
||||||
|
close(r.retrieveErrors)
|
||||||
|
}
|
8
pkg/util/locatejavahome_unix.go
Normal file
8
pkg/util/locatejavahome_unix.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
func LocateJavaHome(component string, majorVersion int) (string, error) {
|
||||||
|
if component == "jre-legacy" {
|
||||||
|
return "/usr/lib/jvm/java-8-openjdk/jre", nil
|
||||||
|
}
|
||||||
|
return "/usr/lib/jvm/default", nil
|
||||||
|
}
|
68
pkg/util/locatejavahome_windows.go
Normal file
68
pkg/util/locatejavahome_windows.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const legacyJavaRegPath = "SOFTWARE\\JavaSoft\\Java Runtime Environment"
|
||||||
|
const jdkRegPath = "SOFTWARE\\JavaSoft\\JDK"
|
||||||
|
|
||||||
|
func LocateJavaHome(component string, majorVersion int) (string, error) {
|
||||||
|
if component == "jre-legacy" {
|
||||||
|
jre, err := registry.OpenKey(registry.LOCAL_MACHINE, legacyJavaRegPath, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
curVer, _, err := jre.GetStringValue("CurrentVersion")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
mVer, err := strconv.ParseInt(strings.Split(curVer, ".")[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if mVer < int64(majorVersion) {
|
||||||
|
return "", fmt.Errorf("installed JRE version %s is older than allowed 1.%d", curVer, majorVersion)
|
||||||
|
}
|
||||||
|
jre, err = registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("%s\\%s", legacyJavaRegPath, curVer), registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
javaHome, _, err := jre.GetStringValue("JavaHome")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return javaHome, nil
|
||||||
|
} else {
|
||||||
|
jdk, err := registry.OpenKey(registry.LOCAL_MACHINE, jdkRegPath, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
curVer, _, err := jdk.GetStringValue("CurrentVersion")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
mVer, err := strconv.ParseInt(strings.Split(curVer, ".")[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if mVer < int64(majorVersion) {
|
||||||
|
return "", fmt.Errorf("installed JDK version %d is older than allowed %d", mVer, majorVersion)
|
||||||
|
}
|
||||||
|
jdk, err = registry.OpenKey(registry.LOCAL_MACHINE, fmt.Sprintf("%s\\%s", jdkRegPath, curVer), registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
javaHome, _, err := jdk.GetStringValue("JavaHome")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return javaHome, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
26
pkg/util/osversion_unix.go
Normal file
26
pkg/util/osversion_unix.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OSVersion() (string, error) {
|
||||||
|
switch osName := runtime.GOOS; osName {
|
||||||
|
case "linux":
|
||||||
|
// Implemented just in case for future. Guess if it will ever be needed
|
||||||
|
// then a kernel version will be used to compare against.
|
||||||
|
osRelease, err := os.ReadFile("/proc/sys/kernel/osrelease")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
kVer := strings.Split(string(osRelease), "-")
|
||||||
|
if len(kVer) >= 1 {
|
||||||
|
return kVer[0], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("malformed osrelease")
|
||||||
|
}
|
||||||
|
return "", errors.New("unknown OS")
|
||||||
|
}
|
29
pkg/util/osversion_windows.go
Normal file
29
pkg/util/osversion_windows.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OSVersion() (string, error) {
|
||||||
|
switch osName := runtime.GOOS; osName {
|
||||||
|
case "windows":
|
||||||
|
verKey, err := registry.OpenKey(registry.LOCAL_MACHINE, "Software\\Microsoft\\Windows NT\\CurrentVersion", registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
major, _, err := verKey.GetIntegerValue("CurrentMajorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
minor, _, err := verKey.GetIntegerValue("CurrentMinorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d.%d", major, minor), nil
|
||||||
|
}
|
||||||
|
return "", errors.New("unknown OS")
|
||||||
|
}
|
145
pkg/util/util.go
Executable file
145
pkg/util/util.go
Executable file
@ -0,0 +1,145 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Sha1 string `json:"sha1"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Retrieve() ([]byte, error) {
|
||||||
|
return GetFromUrl(f.Url, f.Sha1, "sha1", f.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) GetName() string {
|
||||||
|
if f.Id == "" {
|
||||||
|
return f.Path
|
||||||
|
}
|
||||||
|
return f.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFromUrl(url string, hash, hashType string, fSize int64) ([]byte, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("status code returned is %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if fSize != -1 && resp.ContentLength != -1 && resp.ContentLength != fSize {
|
||||||
|
return nil, fmt.Errorf("response size mismatch. %d != %d", resp.ContentLength, fSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fSize != -1 && len(data) != int(fSize) {
|
||||||
|
return nil, fmt.Errorf("body size mismatch. %d != %d", len(data), fSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hash != "" {
|
||||||
|
if err := CheckHash(data, hash, hashType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFileExist(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(fPath string, data []byte) error {
|
||||||
|
if err := os.MkdirAll(path.Dir(fPath), 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(fPath, data, 0666); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFile(fPath string) ([]byte, error) {
|
||||||
|
if !IsFileExist(fPath) {
|
||||||
|
return nil, fmt.Errorf("File %s not found", fPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(fPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadOrDownloadFile will check if file exists and correct, will download it otherwise.
|
||||||
|
func LoadOrDownloadFile(fPath, url string, hash, hashType string, fSize int64) ([]byte, error) {
|
||||||
|
if IsFileExist(fPath) {
|
||||||
|
data, err := os.ReadFile(fPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hash != "" {
|
||||||
|
if err := CheckHash(data, hash, hashType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetFromUrl(url, hash, hashType, fSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckHash(data []byte, fHash, hashType string) error {
|
||||||
|
var hasher hash.Hash
|
||||||
|
|
||||||
|
switch hashType {
|
||||||
|
case "sha1":
|
||||||
|
hasher = sha1.New()
|
||||||
|
case "md5":
|
||||||
|
hasher = md5.New()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported hash type %s", hashType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := hasher.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resultedHash := hasher.Sum(nil)
|
||||||
|
|
||||||
|
if (strings.Contains(fHash, "=") && base64.StdEncoding.EncodeToString(resultedHash) == fHash) ||
|
||||||
|
hex.EncodeToString(resultedHash) == fHash {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("hash mismatch: %s != %s", hex.EncodeToString(resultedHash), fHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PathSeparator() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return ";"
|
||||||
|
}
|
||||||
|
return ":"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user