Compare commits

...

82 Commits

Author SHA1 Message Date
olebeck 7a6061dba2 Revert "revert to 1.19.63"
This reverts commit 3bf371d07c.
2023-03-18 12:17:20 +01:00
olebeck 3bf371d07c revert to 1.19.63 2023-03-18 12:15:00 +01:00
olebeck ced2b28d9b clean up 2023-03-18 12:12:54 +01:00
olebeck c8aa5389c1 add markVariant 2023-03-15 19:33:05 +01:00
olebeck 525a5e64fe fix wrong update path 2023-03-15 16:14:57 +01:00
olebeck 4cc99f5753 update dragonfly 2023-03-15 15:58:58 +01:00
olebeck 3bba95dc64 voidGen default in gui 2023-03-15 15:36:07 +01:00
olebeck 105a318aa8 fix world resourcepacks not applying 2023-03-15 15:34:18 +01:00
olebeck 7ddec1b09d rescaled icon, fix app exit 2023-03-15 15:01:34 +01:00
olebeck 3c87663d02 add icon, fix bug in dark theme 2023-03-15 14:51:31 +01:00
olebeck 04293a74c5 properly apply entity scale 2023-03-15 12:51:30 +01:00
olebeck a746dee75b add dark palette 2023-03-15 02:36:55 +01:00
olebeck 359ad83fc0 1.19.70 2023-03-15 01:29:43 +01:00
olebeck 3fbfbcf2fc add a basic zoom and pan 2023-03-14 16:32:18 +01:00
olebeck e51bcc6b7f go is picky 2023-03-14 02:36:54 +01:00
olebeck 31e6daba3a fix 2023-03-14 02:35:44 +01:00
olebeck 710723aedb missing linux deps 2023-03-14 02:14:28 +01:00
olebeck bd8e3fcddb fix compile errors 2023-03-14 02:10:16 +01:00
olebeck a6488c98c9 gui rewrite, enable entities 2023-03-14 02:07:39 +01:00
olebeck a163c6757c improve verbose logger 2023-03-11 19:50:13 +01:00
olebeck 049a9a6d48 add blind proxy 2023-03-11 17:18:55 +01:00
olebeck 006f604436 fix issue with subchunk servers being labelled as empty 2023-03-11 16:50:54 +01:00
olebeck 8b763cde79 add skinid to customdata fields 2023-03-11 16:05:26 +01:00
olebeck b73ef84c1f fix action 2023-03-10 10:52:59 +01:00
olebeck f2dc5000dc fix env 2023-03-09 16:24:39 +01:00
olebeck c5e7237273 env not properly applied 2023-03-09 14:58:54 +01:00
olebeck c94d7038e9 update ci 2023-03-08 17:35:51 +01:00
olebeck 2bf6f3c534 typo. 2023-03-08 12:46:38 +01:00
olebeck 12ee9d2bcf make the worlds ui semi usable 2023-03-08 12:46:16 +01:00
olebeck c9850772d5 rework ui code to allow writing uis for subcommands 2023-03-06 21:49:30 +01:00
olebeck 5565301f11 fix accidental includes, some settings not showing 2023-03-06 17:55:36 +01:00
olebeck f643e59e3f wrong path for windows 2023-03-06 17:03:45 +01:00
olebeck 818aa73a3e add android build 2023-03-06 16:47:02 +01:00
olebeck 06cf948913 add arch selection to build script, send current version when fetching updates 2023-03-06 16:17:01 +01:00
olebeck ee156a1271 seperate gui / cli builds 2023-03-06 15:50:36 +01:00
olebeck fbb2305d1c disable it actually 2023-03-06 11:56:28 +01:00
olebeck 3e6da4ebfe disabled go vcs 2023-03-06 11:54:00 +01:00
olebeck f661895d2f install fyne 2023-03-06 02:05:33 +01:00
olebeck 5a54b98fd7 update ci to build gui 2023-03-06 02:03:31 +01:00
olebeck 5b5ddc92bb add a gui settings thing 2023-03-05 22:50:58 +01:00
olebeck e8062430e7 update dragonfly 2023-03-05 20:44:04 +01:00
olebeck c93cf7d0f9 fix full image render 2023-03-05 20:08:30 +01:00
olebeck 17572e8e94 fix wrong skins being saved, save skins immediately 2023-03-04 03:25:13 +01:00
olebeck 5485182135 1.19.63 2023-02-26 02:19:10 +01:00
olebeck 8365adb4cd add deviceID to custom data 2023-02-23 10:23:35 +01:00
olebeck d6b2f53f48 dont ignore some errors 2023-02-22 17:47:40 +01:00
olebeck e53208969e allow connection errors 2023-02-22 16:48:24 +01:00
olebeck 6acb821a5a update gophertunnel 2023-02-22 11:22:39 +01:00
olebeck 956448d707 fix build 2023-02-18 21:49:44 +01:00
olebeck f099996de7 update gophertunnel (support 1.19.62) 2023-02-18 20:39:45 +01:00
olebeck 8450304c72 update gophertunnel 2023-02-13 01:44:41 +01:00
olebeck c4cc819dc2 update gophertunnel 2023-02-13 01:32:27 +01:00
olebeck 6670d575d7 allow custom user data (untested) 2023-02-12 22:22:44 +01:00
olebeck 4d4eb37647 oops 2023-02-08 20:02:05 +01:00
olebeck 55916e4e55 rewrite skins to output skin packs 2023-02-08 19:08:26 +01:00
olebeck 2251d7096d go.sum 2023-02-08 12:00:54 +01:00
olebeck a306a6e77f fix map item 2023-02-05 17:41:06 +01:00
olebeck a601cb33a0 fix pixelparadise bug 2023-02-05 02:03:14 +01:00
olebeck aed76d6372 fix issue with cubecraft 2023-01-29 23:07:42 +01:00
olebeck 3a5a13aa75 code formatting 2023-01-29 21:20:13 +00:00
olebeck 32f1e8019a add custom items, (untested probably not working entities) 2023-01-29 15:08:41 +00:00
olebeck ea6dea7953 add custom items, use proxy code for everything 2023-01-29 01:44:32 +00:00
olebeck e84b3ea2fe fix skins command not properly closing, extra-debug now force enables debug 2023-01-28 23:59:58 +00:00
olebeck 0576f3ce63 add extremely verbose packet log 2023-01-27 13:07:53 +01:00
olebeck ec5bae8b8f fix blocknbt 2023-01-26 16:04:32 +01:00
olebeck a4affc7d23 fix issue saving custom block worlds, may have issues above 1.17 still 2023-01-26 15:30:18 +01:00
olebeck a4a293ef52 enable creator experiments in the world if custom blocks exist 2023-01-25 22:12:49 +01:00
olebeck 2d3650f001 update dragonfly 2023-01-25 15:15:48 +01:00
olebeck 9e99a9206b fix mistakes in german translation 2023-01-25 14:13:07 +01:00
olebeck 2f9c74ae5c update dragonfly, put captures in folder 2023-01-25 13:51:00 +01:00
olebeck 12417c63a3 update dragonfly 2023-01-24 22:41:49 +01:00
olebeck d5dec0af98 update dragonfly 2023-01-24 22:41:37 +01:00
olebeck 56ac765b1d fixing behavior pack textures 2023-01-24 12:50:27 +01:00
olebeck 3dba4525b5 generate proper behavior packs, chatgpt translation to german 2023-01-24 10:49:48 +01:00
olebeck 1cc242680a save bp to folder 2023-01-23 20:03:21 +01:00
olebeck 1537c7f705 hopefully this actually fixed it 2023-01-23 19:48:35 +01:00
olebeck 1160e025ec fix translations 2023-01-23 19:18:01 +01:00
olebeck 7d2974c9c4
Merge pull request #30 from itz-depression/master
added owo language
2023-01-23 17:18:08 +01:00
itz_Deprestion 75396f908f
Update owo.yaml 2023-01-23 16:13:42 +00:00
itz_Deprestion 77d47124c8
added owo language 2023-01-23 15:11:50 +00:00
olebeck f80061543c add missing translations 2023-01-23 13:50:42 +01:00
olebeck 47f85f78b3 updating dependencies, adding translation support 2023-01-23 13:40:12 +01:00
69 changed files with 5104 additions and 1428 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
utils/resourcepack-ace.go.ignore filter=git-crypt diff=git-crypt
subcommands/resourcepack-d/resourcepack-d.go filter=git-crypt diff=git-crypt

View File

@ -1,6 +1,8 @@
name: ci-build
on:
push:
branches:
- '**'
workflow_dispatch:
jobs:
@ -13,51 +15,56 @@ jobs:
fetch-depth: 0
- run: |
git fetch --force --tags
- uses: actions/setup-go@v3
- name: Setup Golang with cache
uses: magnetikonline/action-golang-cache@v3
with:
go-version: '1.19'
check-latest: true
cache: true
go-version-file: go.mod
- name: Install SSH Key
if: ${{ env.SSH_PRIVATE_KEY != '' }}
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
- uses: awalsh128/cache-apt-pkgs-action@latest
if: ${{ env.REPO_KEY != '' }}
with:
packages: git-crypt xxd
packages: git-crypt xxd gcc pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev
version: 1.0
env:
REPO_KEY: ${{ secrets.REPO_KEY }}
- name: decrypt
if: ${{ !env.ACT }}
if: ${{ env.REPO_KEY != '' }}
run: |
echo ${REPO_KEY} | xxd -r -p > ../bedrock-repo-key.key
git-crypt unlock ../bedrock-repo-key.key
rm ../bedrock-repo-key.key
env:
REPO_KEY: ${{ secrets.REPO_KEY }}
- run: go install github.com/sanbornm/go-selfupdate/cmd/go-selfupdate
- run: go get ./cmd/bedrocktool
- name: Install SSH Key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}
- name: dependencies
run: |
go get ./cmd/bedrocktool
go install gioui.org/cmd/gogio@latest
- name: build
id: build
run: |
make -j dists updates
run: python build.py
- name: Deploy with rsync
run: rsync -avz ./public/ olebeck@${{ secrets.SSH_HOST }}:/var/www/updates/bedrocktool/
- name: 'Get Previous tag'
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
if: ${{ env.SSH_HOST != '' }}
run: rsync -avzO ./updates/ olebeck@${SSH_HOST}:/var/www/updates/
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: ${{ steps.previoustag.outputs.tag }}
files: dist/*
automatic_release_tag: ${{ steps.build.outputs.release_tag }}
files: ./builds/*
prerelease: false

11
.gitignore vendored
View File

@ -8,6 +8,7 @@ token.json
*.bmp
*.bin
*.log
*.syso
/bedrocktool
/bedrocktool-*
@ -17,6 +18,12 @@ __debug_bin
keys.db
/skins/
/worlds/
/packs/
/dist/
/public/
/builds/
/builds/
/updates/
/fyne-cross/
/tmp/
packets.log.gpg
customdata.json

View File

@ -1,59 +0,0 @@
TAG = $(shell git describe --exclude "r-*" --tags --always)
NAME = bedrocktool-${TAG}
SRCS = $(wildcard **/*.go)
GC = go build -ldflags "-s -w -X github.com/bedrock-tool/bedrocktool/utils.Version=${TAG}"
.PHONY: dists clean updates
# check if packs are supported
HAVE_PACKS = false
ifeq ($(shell head -c 7 ./utils/resourcepack-ace.go.ignore),package)
HAVE_PACKS = true
endif
$(info pack support: ${HAVE_PACKS})
ifeq ($(HAVE_PACKS),true)
GC += -overlay overlay.json
endif
bedrocktool: $(SRCS)
$(GC) -o $@ ./cmd/bedrocktool
BUILDS=\
windows_386.exe\
windows_amd64.exe\
windows_arm64.exe\
windows_arm.exe\
darwin_amd64\
darwin_arm64\
linux_386\
linux_amd64\
linux_arm64\
linux_arm
DISTS=$(BUILDS:%=dist/$(NAME)_%)
dists: $(DISTS)
$(DISTS): OS = $(word 2,$(subst _, ,$@))
$(DISTS): ARCH = $(word 1,$(subst ., ,$(word 3,$(subst _, ,$@))))
$(DISTS): BUILD = builds/$(OS)-$(ARCH)
dist builds:
mkdir -p dist builds
$(DISTS): dist builds $(SRCS)
$(info building: $@)
GOOS=$(OS) GOARCH=$(ARCH) $(GC) -o $(BUILD) ./cmd/bedrocktool
cp $(BUILD) $@
UPDATES=$(BUILDS)
$(UPDATES): OS = $(word 1,$(subst _, ,$@))
$(UPDATES): ARCH = $(word 1,$(subst ., ,$(word 2,$(subst _, ,$@))))
updates: $(UPDATES)
$(UPDATES): $(DISTS)
go-selfupdate -platform $(OS)-$(ARCH) builds/ $(TAG)
clean:
rm -r dist builds public

189
build.py Normal file
View File

@ -0,0 +1,189 @@
import subprocess, re, sys, os, shutil, json, binascii, hashlib, gzip
VER_RE = re.compile(r"v(\d\.\d+\.\d+)(?:-(\d+)-(\w))?")
NAME = "bedrocktool"
APP_ID = "yuv.pink.bedrocktool"
GIT_TAG = subprocess.run(["git", "describe", "--exclude", "r*", "--tags", "--always"], stdout=subprocess.PIPE).stdout.decode("utf8").split("\n")[0]
if GIT_TAG == "":
GIT_TAG = "v0.0.0"
VER_MATCH = VER_RE.match(GIT_TAG)
VER = VER_MATCH.group(1)
PATCH = VER_MATCH.group(2) or "0"
TAG = f"{VER}-{PATCH}"
print(f"VER: {VER}")
print(f"TAG: {TAG}")
GITHUB_OUTPUT = os.getenv("GITHUB_OUTPUT")
if GITHUB_OUTPUT:
with open(GITHUB_OUTPUT, "a") as f:
f.write(f"release_tag=r{VER}\n")
with open("./subcommands/resourcepack-d/resourcepack-d.go", "rb") as f:
PACK_SUPPORT = f.read(100).count(b"package ") > 0
print(f"Pack Support: {PACK_SUPPORT}")
LDFLAGS = f"-s -w -X github.com/bedrock-tool/bedrocktool/utils.Version={TAG}"
PLATFORMS = [
("windows", ["amd64"], ".exe"),
("linux", ["amd64"], ""),
#("darwin", ["amd64", "arm64"], ""),
#("android", ["arm64"], ".apk")
]
def clean():
if os.path.exists("./tmp"):
shutil.rmtree("./tmp")
if os.path.exists("./builds"):
shutil.rmtree("./builds")
if os.path.exists("./updates"):
shutil.rmtree("./updates")
for file in os.listdir("./cmd/bedrocktool"):
if file.endswith(".syso"):
os.remove(f"./cmd/bedrocktool/{file}")
def make_dirs():
os.mkdir("./tmp")
os.mkdir("./builds")
os.mkdir("./updates")
def build_cli(platform: str, arch: str, env_in: dict[str,str], tags: list[str], ldflags, compiled_path: str):
env = {}
env.update(env_in)
env.update({
"GOOS": platform,
"GOARCH": arch,
})
args = [
"go", "build",
"-ldflags", ldflags,
"-trimpath",
"-tags", ",".join(tags),
"-o", compiled_path,
"-v"
]
args.append("./cmd/bedrocktool")
env2 = os.environ.copy()
env2.update(env)
subprocess.run(args, env=env2).check_returncode()
def build_gui(platform: str, arch: str, env, tags: list[str], ldflags, compiled_path: str):
if platform == "windows":
ldflags += " -H=windows"
args = [
"gogio",
"-arch", arch,
"-target", platform,
"-icon", "icon.png",
"-tags", ",".join(tags),
"-ldflags", ldflags,
"-o", compiled_path,
"-x"
]
if platform in ["android", "ios"]:
args.extend(["-appid", APP_ID])
args.append("./cmd/bedrocktool")
env2 = os.environ.copy()
env2.update(env)
subprocess.run(args, env=env2).check_returncode()
def package(platform: str, arch: str, compiled_path: str, GUI: bool, ext: str):
SUB1 = '-gui' if GUI else ''
exe_out_path = f"./builds/{NAME}-{platform}-{arch}-{TAG}{SUB1}{ext}"
# create hash and copy
with open(compiled_path, "rb") as f:
exe_data = f.read()
sha = binascii.b2a_base64(hashlib.sha256(exe_data).digest()).decode("utf8").split("\n")[0]
shutil.copy(compiled_path, exe_out_path)
# create update
updates_dir = f"./updates/{NAME}{SUB1}"
os.makedirs(updates_dir, exist_ok=True)
with open(f"{updates_dir}/{platform}-{arch}.json", "w") as f:
f.write(json.dumps({
"Version": TAG,
"Sha256": sha,
}, indent=2))
# write update data
os.makedirs(f"{updates_dir}/{TAG}", exist_ok=True)
with gzip.open(f"{updates_dir}/{TAG}/{platform}-{arch}.gz", "wb") as f:
f.write(exe_data)
os.remove(compiled_path)
def build_all(platform_filter: str, arch_filter: str):
for (platform, archs, ext) in PLATFORMS:
if platform_filter and platform_filter != platform:
continue
archs = [a for a in archs if arch_filter == "" or a == arch_filter]
if len(archs) == 0:
continue
for GUI in [False, True]:
if platform in ["android"] and not GUI:
continue
print(f"Building {platform} gui: {GUI}")
SUB1 = '-gui' if GUI else ''
name = f"{NAME}{SUB1}"
tags = []
if PACK_SUPPORT:
tags.append("packs")
if GUI:
tags.append("gui")
env = {
"GOVCS": "*:off"
}
ldflags = LDFLAGS
if tags.count("gui"):
CmdName = "bedrocktool-gui"
else:
CmdName = "bedrocktool"
ldflags += f" -X github.com/bedrock-tool/bedrocktool/utils.CmdName={CmdName}"
for arch in archs:
compiled_path = f"./tmp/{platform}-{arch}{SUB1}/{name}{ext}"
os.makedirs(os.path.dirname(compiled_path), exist_ok=True)
if GUI and platform != "linux":
build_gui(platform, arch, env, tags, ldflags, compiled_path)
else:
build_cli(platform, arch, env, tags, ldflags, compiled_path)
package(platform, arch, compiled_path, GUI, ext)
def main():
platform_filter = ""
arch_filter = ""
if len(sys.argv) > 1:
platform_filter = sys.argv[1]
if len(sys.argv) > 2:
arch_filter = sys.argv[2]
if platform_filter == "clean":
clean()
return
clean()
make_dirs()
build_all(platform_filter, arch_filter)
main()

View File

@ -8,36 +8,55 @@ import (
"os"
"os/signal"
"runtime/debug"
"strings"
"syscall"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"gopkg.in/square/go-jose.v2/json"
_ "github.com/bedrock-tool/bedrocktool/subcommands"
_ "github.com/bedrock-tool/bedrocktool/subcommands/skins"
_ "github.com/bedrock-tool/bedrocktool/subcommands/world"
_ "github.com/bedrock-tool/bedrocktool/ui"
"github.com/google/subcommands"
"github.com/sirupsen/logrus"
)
type CLI struct {
utils.BaseUI
}
func (c *CLI) Init() bool {
utils.SetCurrentUI(c)
return true
}
func (c *CLI) Start(ctx context.Context, cancel context.CancelFunc) error {
flag.Parse()
utils.InitDNS()
utils.InitExtraDebug(ctx)
subcommands.Execute(ctx)
return nil
}
func main() {
defer func() {
if err := recover(); err != nil {
logrus.Errorf("Fatal Error occurred.")
logrus.Errorf(locale.Loc("fatal_error", nil))
println("")
println("--COPY FROM HERE--")
logrus.Infof("Version: %s", utils.Version)
logrus.Infof("Cmdline: %s", os.Args)
logrus.Errorf("Error: %s", err)
fmt.Println("stacktrace from panic: \n" + string(debug.Stack()))
println("stacktrace from panic: \n" + string(debug.Stack()))
println("--END COPY HERE--")
println("")
println("if you want to report this error, please open an issue at")
println("https://github.com/bedrock-tool/bedrocktool/issues")
println("And attach the error info, describe what you did to get this error.")
println("Thanks!\n")
if utils.G_interactive {
println(locale.Loc("report_issue", nil))
if utils.Options.ExtraDebug {
println(locale.Loc("used_extra_debug_report", nil))
}
if utils.Options.IsInteractive {
input := bufio.NewScanner(os.Stdin)
input.Scan()
}
@ -47,7 +66,7 @@ func main() {
logrus.SetLevel(logrus.DebugLevel)
if utils.Version != "" {
logrus.Infof("bedrocktool version: %s", utils.Version)
logrus.Infof(locale.Loc("bedrocktool_version", locale.Strmap{"Version": utils.Version}))
}
newVersion, err := utils.Updater.UpdateAvailable()
@ -56,14 +75,18 @@ func main() {
}
if newVersion != "" && utils.Version != "" {
logrus.Infof("Update Available: %s", newVersion)
logrus.Infof(locale.Loc("update_available", locale.Strmap{"Version": newVersion}))
}
ctx, cancel := context.WithCancel(context.Background())
flag.BoolVar(&utils.G_debug, "debug", false, "debug mode")
flag.BoolVar(&utils.G_preload_packs, "preload", false, "preload resourcepacks for proxy")
enable_dns := flag.Bool("dns", false, "enable dns server for consoles")
flag.StringVar(&utils.RealmsEnv, "realms-env", "", "realms env")
flag.BoolVar(&utils.Options.Debug, "debug", false, locale.Loc("debug_mode", nil))
flag.BoolVar(&utils.Options.Preload, "preload", false, locale.Loc("preload_packs", nil))
flag.BoolVar(&utils.Options.ExtraDebug, "extra-debug", false, locale.Loc("extra_debug", nil))
flag.StringVar(&utils.Options.PathCustomUserData, "userdata", "", locale.Loc("custom_user_data", nil))
flag.String("lang", "", "lang")
flag.BoolVar(&utils.Options.EnableDNS, "dns", false, locale.Loc("enable_dns", nil))
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.ImportantFlag("debug")
@ -71,33 +94,13 @@ func main() {
subcommands.ImportantFlag("preload")
subcommands.HelpCommand()
{ // interactive input
if len(os.Args) < 2 {
select {
case <-ctx.Done():
return
default:
fmt.Println("Available commands:")
for name, desc := range utils.ValidCMDs {
fmt.Printf("\t%s\t%s\n", name, desc)
}
fmt.Printf("Use '%s <command>' to run a command\n", os.Args[0])
var ui utils.UI
cmd, cancelled := utils.User_input(ctx, "Input Command: ")
if cancelled {
return
}
_cmd := strings.Split(cmd, " ")
os.Args = append(os.Args, _cmd...)
utils.G_interactive = true
}
}
}
flag.Parse()
if *enable_dns {
utils.InitDNS()
if len(os.Args) < 2 {
ui = utils.MakeGui()
utils.Options.IsInteractive = true
} else {
ui = &CLI{}
}
// exit cleanup
@ -109,12 +112,14 @@ func main() {
cancel()
}()
subcommands.Execute(ctx)
if utils.G_interactive {
logrus.Info("Press Enter to exit.")
input := bufio.NewScanner(os.Stdin)
input.Scan()
if !ui.Init() {
logrus.Error("Failed to init UI!")
return
}
err = ui.Start(ctx, cancel)
cancel()
if err != nil {
logrus.Error(err)
}
}
@ -124,31 +129,52 @@ type TransCMD struct {
func (*TransCMD) Name() string { return "trans" }
func (*TransCMD) Synopsis() string { return "" }
func (c *TransCMD) SetFlags(f *flag.FlagSet) {
f.BoolVar(&c.auth, "auth", false, "if it should login to xbox")
f.BoolVar(&c.auth, "auth", false, locale.Loc("should_login_xbox", nil))
}
func (c *TransCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n"
}
func (c *TransCMD) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
func (c *TransCMD) Execute(_ context.Context, ui utils.UI) error {
const (
BLACK_FG = "\033[30m"
BOLD = "\033[1m"
BLUE = "\033[46m"
PINK = "\033[45m"
WHITE = "\033[47m"
RESET = "\033[0m"
BlackFg = "\033[30m"
Bold = "\033[1m"
Blue = "\033[46m"
Pink = "\033[45m"
White = "\033[47m"
Reset = "\033[0m"
)
if c.auth {
utils.GetTokenSource()
}
fmt.Println(BLACK_FG + BOLD + BLUE + " Trans " + PINK + " Rights " + WHITE + " Are " + PINK + " Human " + BLUE + " Rights " + RESET)
return 0
fmt.Println(BlackFg + Bold + Blue + " Trans " + Pink + " Rights " + White + " Are " + Pink + " Human " + Blue + " Rights " + Reset)
return nil
}
type CreateCustomDataCMD struct {
path string
}
func (*CreateCustomDataCMD) Name() string { return "create-customdata" }
func (*CreateCustomDataCMD) Synopsis() string { return "" }
func (c *CreateCustomDataCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.path, "path", "customdata.json", "where to save")
}
func (c *CreateCustomDataCMD) Execute(_ context.Context, ui utils.UI) error {
var data utils.CustomClientData
fio, err := os.Create(c.path)
if err == nil {
defer fio.Close()
var bdata []byte
bdata, err = json.MarshalIndent(&data, "", "\t")
fio.Write(bdata)
}
if err != nil {
return err
}
return nil
}
func init() {
utils.RegisterCommand(&TransCMD{})
utils.RegisterCommand(&CreateCustomDataCMD{})
}

60
go.mod
View File

@ -1,54 +1,66 @@
module github.com/bedrock-tool/bedrocktool
go 1.19
go 1.20
//replace github.com/sandertv/gophertunnel => ./gophertunnel
replace github.com/sandertv/gophertunnel => github.com/olebeck/gophertunnel v1.26.1
replace github.com/sandertv/gophertunnel => github.com/olebeck/gophertunnel v1.27.4-3
//replace github.com/df-mc/dragonfly => ./dragonfly
replace github.com/df-mc/dragonfly => github.com/olebeck/dragonfly v0.8.10-1
replace github.com/df-mc/dragonfly => github.com/olebeck/dragonfly v0.9.3-3
require (
github.com/df-mc/dragonfly v0.8.5
gioui.org v0.0.0-20221219171716-c455f0f342ef
gioui.org/x v0.0.0-20230313161557-05b40af72ed0
github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/df-mc/dragonfly v0.9.3
github.com/df-mc/goleveldb v1.1.9
github.com/fatih/color v1.13.0
github.com/flytam/filenamify v1.1.1
github.com/fatih/color v1.14.1
github.com/flytam/filenamify v1.1.2
github.com/go-gl/mathgl v1.0.0
github.com/google/subcommands v1.2.0
github.com/miekg/dns v1.1.50
github.com/google/uuid v1.3.0
github.com/jinzhu/copier v0.3.5
github.com/miekg/dns v1.1.51
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/sanbornm/go-selfupdate v0.0.0-20210106163404-c9b625feac49
github.com/sandertv/gophertunnel v1.26.0
github.com/sandertv/go-raknet v1.12.0
github.com/sandertv/gophertunnel v1.27.4
github.com/sirupsen/logrus v1.9.0
golang.design/x/lockfree v0.0.1
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
golang.org/x/crypto v0.7.0
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017
golang.org/x/image v0.6.0
golang.org/x/oauth2 v0.6.0
golang.org/x/text v0.8.0
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v3 v3.0.1
)
require (
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
gioui.org/shader v1.0.6 // indirect
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/benoitkugler/textlayout v0.3.0 // indirect
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 // indirect
github.com/changkun/lockfree v0.0.1 // indirect
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 // indirect
github.com/df-mc/atomic v1.10.0 // indirect
github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0
github.com/jinzhu/copier v0.3.5
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/klauspost/compress v1.15.11 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/kr/binarydist v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/muhammadmuzzammil1998/jsonc v1.0.0 // indirect
github.com/sandertv/go-raknet v1.12.0 // indirect
github.com/sirupsen/logrus v1.9.0
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inconshreveable/go-update.v0 v0.0.0-20150814200126-d8b0b1d421aa // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
)

150
go.sum
View File

@ -1,7 +1,30 @@
eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 h1:djFprmHZgrSepsHAIRMp5UJn3PzsoTg9drI+BDmif5Q=
gioui.org v0.0.0-20221219171716-c455f0f342ef h1:0nxeswOamzSRIjFjRmASp+sWCAFzVt7UCC3HlRHIB6k=
gioui.org v0.0.0-20221219171716-c455f0f342ef/go.mod h1:3lLo7xMHYnnHTrgKNNctBjEKKH3wQCO2Sn7ti5Jy8mU=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/x v0.0.0-20230313161557-05b40af72ed0 h1:jjdcmnjM8R1yhiBBbPinm5TLNs6uRei25fjnvgYBTjU=
gioui.org/x v0.0.0-20230313161557-05b40af72ed0/go.mod h1:kmHRtak7XgGZYYuqFgDBJ42PbQjPrV/xOpw9FLx3eVY=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
github.com/benoitkugler/textlayout v0.3.0 h1:2ehWXEkgb6RUokTjXh1LzdGwG4dRP6X3dqhYYDYhUVk=
github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
github.com/benoitkugler/textlayout-testdata v0.1.1 h1:AvFxBxpfrQd8v55qH59mZOJOQjtD6K2SFe9/HvnIbJk=
github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 h1:/G0ghZwrhou0Wq21qc1vXXMm/t/aKWkALWwITptKbE0=
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM=
github.com/changkun/lockfree v0.0.1 h1:5WefVJLglY4IHRqOQmh6Ao6wkJYaJkarshKU8VUtId4=
github.com/changkun/lockfree v0.0.1/go.mod h1:3bKiaXn/iNzIPlSvSOMSVbRQUQtAp8qUAyBUtzU11s4=
github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:Yg2hDs4b13Evkpj42FU2idX2cVXVFqQSheXYKM86Qsk=
github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:MgJyK38wkzZbiZSKeIeFankxxSA8gayko/nr5x5bgBA=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -9,13 +32,15 @@ github.com/df-mc/atomic v1.10.0 h1:0ZuxBKwR/hxcFGorKiHIp+hY7hgY+XBTzhCYD2NqSEg=
github.com/df-mc/atomic v1.10.0/go.mod h1:Gw9rf+rPIbydMjA329Jn4yjd/O2c/qusw3iNp4tFGSc=
github.com/df-mc/goleveldb v1.1.9 h1:ihdosZyy5jkQKrxucTQmN90jq/2lUwQnJZjIYIC/9YU=
github.com/df-mc/goleveldb v1.1.9/go.mod h1:+NHCup03Sci5q84APIA21z3iPZCuk6m6ABtg4nANCSk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/flytam/filenamify v1.1.1 h1:508gP8QR6vtbv46S3oz2ob9l7JGFdDFfqqMeh/TwzTk=
github.com/flytam/filenamify v1.1.1/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/flytam/filenamify v1.1.2 h1:dGlfWU4zrhDlsmvob4IFcfgjG5vIjfo4UwLyec6Wx94=
github.com/flytam/filenamify v1.1.2/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 h1:iOA0HmtpANn48hX2nlDNMu0VVaNza35HJG0WeetBVzQ=
github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5/go.mod h1:/cmOXaoTiO+lbCwkTZBgCvevJpbFsZ5reXIpEJVh5MI=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@ -37,35 +62,29 @@ github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo=
github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/muhammadmuzzammil1998/jsonc v1.0.0 h1:8o5gBQn4ZA3NBA9DlTujCj2a4w0tqWrPVjDwhzkgTIs=
github.com/muhammadmuzzammil1998/jsonc v1.0.0/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/olebeck/dragonfly v0.8.5-1 h1:+z1WO0W4jCc1jcPMGDz85AxLWz59haEakD0yAVEXeWQ=
github.com/olebeck/dragonfly v0.8.5-1/go.mod h1:CHQ21j4LjSwHgUeMk06tcxEW6U+SI7avLCUeSvp9AXo=
github.com/olebeck/dragonfly v0.8.10-1 h1:oMeCcyoKfb9xEDxxbKM6YyUPlK/R8wix/zzZ4UzkYYQ=
github.com/olebeck/dragonfly v0.8.10-1/go.mod h1:ZjzPME6I1nc73voUgr2s5lpkoTxnWuR54V6c1KbULX0=
github.com/olebeck/gophertunnel v1.24.11-1 h1:DKEqjK5nAcjlwejHAD/SHeolAM6y9EkyjJcMuoK7VVY=
github.com/olebeck/gophertunnel v1.24.11-1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/olebeck/gophertunnel v1.24.13-1 h1:3Sj22hVFAGCnPMJAewoSeuC9N8ikzcWo0DvJNBnXvs0=
github.com/olebeck/gophertunnel v1.24.13-1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/olebeck/gophertunnel v1.25.0-1 h1:9vh6MIUo7/3EJI5oULr5F9KutAoa9qnpgWa566fImaI=
github.com/olebeck/gophertunnel v1.25.0-1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/olebeck/gophertunnel v1.26.1 h1:mkIthqpU5vSksh/mxUlhqFN/nsK9CcP9hAn1LjjqKnQ=
github.com/olebeck/gophertunnel v1.26.1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/olebeck/dragonfly v0.9.3-2 h1:FBO5raxeQvA286L+mUhnAsUSWxVfKmVFoofw0KdJlXA=
github.com/olebeck/dragonfly v0.9.3-2/go.mod h1:ODAzVcmM7KvKgPB89hoYndQiVVgTm9FlDXUko1H3YVs=
github.com/olebeck/dragonfly v0.9.3-3 h1:sv4TJjdgFoCBRMd5mS0g5NyPeMsVfUifhxIlJPTj6xg=
github.com/olebeck/dragonfly v0.9.3-3/go.mod h1:ODAzVcmM7KvKgPB89hoYndQiVVgTm9FlDXUko1H3YVs=
github.com/olebeck/gophertunnel v1.27.4-1 h1:rtB5wksJBKk3sy9lBFZ1aH2giCathINj4zQkl1rPTMg=
github.com/olebeck/gophertunnel v1.27.4-1/go.mod h1:ekREo7U9TPHh86kbuPMaWA93NMyWsfVvP/iNT3XhAb8=
github.com/olebeck/gophertunnel v1.27.4-3 h1:RktAdTNTvCFn6PQou0H3RyqrTo3/xH0bqODrHb/oXAs=
github.com/olebeck/gophertunnel v1.27.4-3/go.mod h1:ekREo7U9TPHh86kbuPMaWA93NMyWsfVvP/iNT3XhAb8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -82,101 +101,86 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.design/x/lockfree v0.0.1 h1:IHFNwZgM5bnZYWkEbzn5lWHMYr8WsRBdCJ/RBVY0xMM=
golang.design/x/lockfree v0.0.1/go.mod h1:iaZUx6UgZaOdePjzI6wFd+seYMl1i0rsG8+xKvA8c4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017 h1:3Ea9SZLCB0aRIhSEjM+iaGIlzzeDJdpi579El/YIhEE=
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b h1:uKO3Js8lXGjpjdc4J3rqs0/Ex5yDKUGfk43tTYWVLas=
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@ -188,5 +192,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

200
locale/de.yaml Normal file
View File

@ -0,0 +1,200 @@
bedrocktool_version:
other: "Bedrocktool Version {{.Version}}"
available_commands:
other: "Verfügbare Befehle:"
use_to_run_command:
other: "Verwenden Sie 'bedrocktool <Befehl>', um einen Befehl auszuführen"
input_command:
other: "Eingabeaufforderung: "
fatal_error:
other: "Schwerer Fehler aufgetreten."
report_issue:
other: |
Wenn Sie diesen Fehler melden möchten, öffnen Sie bitte ein Problem unter
https://github.com/bedrock-tool/bedrocktool/issues
Und fügen Sie die Fehlerinformationen hinzu, beschreiben Sie, was Sie getan haben, um diesen Fehler zu erhalten.
Vielen Dank!
used_extra_debug_report:
other: |
HINWEIS: Sie haben extra-debug verwendet, welches eine packets.log und eine packets.log.gpg Datei erstellt.
Bitte laden Sie nur die packets.log.gpg Datei hoch, wenn Sie ein Problem melden möchten.
packets.log und packets.log.gpg können Account-Informationen enthalten (nie Login-Informationen), daher ist es sicherer, die verschlüsselte Datei hochzuladen.
Diese Datei kann von dem Entwickler gelesen werden, aber nicht von Personen, die die öffentlichen Problemseiten ansehen.
enter_to_exit:
other: "Drücken Sie die Eingabetaste, um das Programm zu beenden."
should_login_xbox:
other: "ob es sich bei Xbox anmelden soll"
enable_dns:
other: "DNS-Server für Konsolen aktivieren"
preload_packs:
other: "Vorladen von Ressourcenpacks für den Proxy"
debug_mode:
other: "Debug-Modus"
refreshed_token:
other: Token wurde aktualisiert
done:
other: Fertig
starting_dns:
other: "Starten des DNS auf {{.Ip}}:53"
failed_to_start_dns:
other: "Fehler beim Starten des DNS-Servers: {{.Err}}"
suggest_bedrockconnect:
other: "Sie müssen möglicherweise Bedrockconnect verwenden"
invalid_server:
other: "Ungültige Serveradresse"
enter_server:
other: "Serveradresse eingeben: "
disconnect:
other: "Verbindung trennen {{.Pk}}"
preloading_packs:
other: "Ressourcenpacks werden vorgeladen"
failed_to_connect:
other: "Verbindung zu {{.Address}} fehlgeschlagen: {{.Err}}"
listening_on:
other: "Hört auf {{.Address}}"
connecting:
other: "Verbindung wird hergestellt mit {{.Address}}"
connected:
other: "Verbunden mit dem Server."
connection_cancelled:
other: "Verbindung abgebrochen"
failed_start_game:
other: "Das Spiel konnte nicht gestartet werden: {{.Err}}"
help_connect:
other: "Starten Sie Minecraft und verbinden Sie sich mit der lokalen IP-Adresse dieses Computers, um fortzufahren"
failed_to_spawn:
other: "Spawn fehlgeschlagen"
not_supported_anymore:
other: "wird nicht mehr unterstützt"
remote_address:
other: "entfernte Serveradresse"
ctrl_c_to_exit:
other: "Drücken Sie Strg+C, um das Programm zu beenden"
server_address_help:
other: |
akzeptierte Serveradressformate:
123.234.123.234
123.234.123.234:19132
realm:<Realmname>
realm:<Realmname>:<Id>
realm_list_line:
other: "Name: {{.Name}}\tid: {{.Id}}"
failed_to_open_output:
other: "Fehler beim Öffnen der Ausgabe"
adding_world:
other: "{{.World}} wird hinzugefügt"
not_found:
other: "{{.World}} nicht gefunden"
need_to_specify_multiple_worlds:
other: "Sie müssen mehr als 1 Welt angeben, um diese zu vereinen"
update_available:
other: "Update verfügbar: {{.Version}} Verwenden Sie den Befehl 'update', um es zu installieren."
no_update:
other: "Kein Update verfügbar."
updating:
other: "Aktualisiere auf {{.Version}}"
updated:
other: "Aktualisiert!"
worldname_set:
other: "worldName ist jetzt {{.Name}}"
void_generator_true:
other: "Verwende Void Generator"
void_generator_false:
other: "Verwende keinen Void Generator"
subchunk_before_chunk:
other: "Der Server hat den Chunk vor dem Subchunk nicht gesendet!"
zoom_level:
other: "Zoom: {{.Level}}"
not_saving_empty:
other: "Speichern wird übersprungen, da die Welt keine Chunks enthält."
saving_world:
one: "Speichere Welt {{.Name}} mit {{.Count}} Chunk"
other: "Speichere Welt {{.Name}} mit {{.Count}} Chunks"
unknown_gamerule:
other: "unbekannte Gamerule: {{.Name}}"
adding_pack:
other: "Füge Ressourcenpaket {{.Name}} hinzu"
using_customblocks:
other: "Verwende Benutzerdefinierte Blöcke"
guessing_version:
other: "konnte die Spieleversion nicht bestimmen, gehe davon aus, dass es > 1.18 ist"
use_setname:
other: "verwende /setname <worldname>\num den Weltnamen zu setzen"
using_under_118:
other: "verwende legacy (< 1.18)"
setname_desc:
other: "setze benutzerdefinierten Namen für diese Welt"
void_desc:
other: "schalte ein/aus ob der Void Generator verwendet werden soll"
popup_chunk_count:
one: "{{.Count}} Chunk geladen\nName: {{.Name}}"
other: "{{.Count}} Chunks geladen\nName: {{.Name}}"
warn_window_closed_not_open:
other: "Schloss Fenster, das nicht geöffnet war"
saved_block_inv:
other: "Blockinventar gespeichert"
save_packs_with_world:
other: "speichere Resourcepacks mit der Welt"
enable_void:
other: "speichere mit Void Generator"
save_image:
other: "speichert am Ende ein PNG der Karte"
test_block_inv:
other: "aktiviere experimentelles Speichern des Blockinventars"
saved:
other: "Gespeichert: {{.Name}}"
empty_chunk:
other: "Leerer Chunk."
only_with_geometry:
other: "speichere nur Skins mit Geometrie"
name_prefix:
other: "speichere nur Spieler, die mit diesem beginnen"
failed_write:
other: "Fehler beim Schreiben von {{.Part}} {{.Path}}: {{.Err}}"
packet_filter:
other: "zu ignorierende Pakete"
save_encrypted:
other: "speichere verschlüsselte Ressourcenpakete"
only_keys:
other: "speichere nur Schlüssel, entschlüssel das Ressourcenpaket nicht"
decrypting_packs:
other: "Entschlüsselung der Ressourcenpakete"
no_resourcepacks:
other: "Kein Ressourcenpaket gesendet"
writing_keys:
other: "Schreibe Schlüssel nach {{.Path}}"
warn_key_exists:
other: "Schlüssel {{.Id}} existiert bereits"
compare_key:
other: "uuid: {{.Id}}, Schlüssel in der DB: {{.Prev}} != neuer Schlüssel {{.Now}}"
decrypting:
other: "Entschlüsselung..."
list_realms_synopsis:
other: "gibt alle Realm aus, auf die Sie Zugriff haben"
capture_synopsis:
other: "protokolliert Pakete in eine pcap-Datei"
chat_log_synopsis:
other: "protokolliert den Chat in eine Datei"
debug_proxy_synopsis:
other: "detaillierte Debug-Pakete"
merge_synopsis:
other: "führt 2 oder mehr Welten zusammen"
update_synopsis:
other: "aktualisiert sich selbst auf die neueste Version"
skins_proxy_synopsis:
other: "lädt Skins von Spielern auf einem Server über einen Proxy herunter"
skins_synopsis:
other: "lädt Skins von Spielern auf einem Server herunter"
world_synopsis:
other: "lädt eine Welt von einem Server herunter"
pack_synopsis:
other: "lädt Ressourcenpakete von einem Server herunter"

201
locale/en.yaml Normal file
View File

@ -0,0 +1,201 @@
bedrocktool_version:
other: "Bedrocktool Version {{.Version}}"
available_commands:
other: "Available Commands:"
use_to_run_command:
other: "Use 'bedrocktool <command>' to run a command"
input_command:
other: "Input Command: "
fatal_error:
other: "Fatal Error occurred."
report_issue:
other: |
if you want to report this error, please open an issue at
https://github.com/bedrock-tool/bedrocktool/issues
And attach the error info, describe what you did to get this error.
Thanks!
used_extra_debug_report:
other: |
NOTE: you have used extra-debug which creates a packets.log and a packets.log.gpg file
please if you want to submit an issue only upload the packets.log.gpg file.
packets.log and packets.log.gpg may include account info (never login info) so it is safer to upload the encrypted file.
that file can be read by the developer and not by people looking at the public issues page.
enter_to_exit:
other: "Press Enter to exit."
should_login_xbox:
other: "if it should login to xbox"
enable_dns:
other: "enable dns server for consoles"
preload_packs:
other: "preload resourcepacks for proxy"
debug_mode:
other: "debug mode"
refreshed_token:
other: "refreshed token"
done:
other: "done"
starting_dns:
other: "Starting dns at {{.Ip}}:53"
failed_to_start_dns:
other: "Failed to start dns server: {{.Err}}"
suggest_bedrockconnect:
other: "you may have to use Bedrockconnect"
invalid_server:
other: "Invalid Server Address"
enter_server:
other: "Enter Server: "
disconnect:
other: "Disconnect {{.Pk}}"
preloading_packs:
other: "Preloading Resourcepacks"
failed_to_connect:
other: "failed to connect to {{.Address}}: {{.Err}}"
listening_on:
other: "Listening on {{.Address}}"
connecting:
other: "Connecting to {{.Address}}"
connected:
other: "Connected to server."
connection_cancelled:
other: "connection cancelled"
failed_start_game:
other: "failed to start game: {{.Err}}"
help_connect:
other: "Open Minecraft and connect to this computers local ip address to continue"
failed_to_spawn:
other: "Failed to Spawn"
not_supported_anymore:
other: "not supported anymore"
remote_address:
other: "remote server address"
ctrl_c_to_exit:
other: "Press ctrl+c to exit"
server_address_help:
other: |
accepted server address formats:
123.234.123.234
123.234.123.234:19132
realm:<Realmname>
realm:<Realmname>:<Id>
realm_list_line:
other: "Name: {{.Name}}\tid: {{.Id}}"
failed_to_open_output:
other: "Failed to open output"
adding_world:
other: "Adding {{.World}}"
not_found:
other: "{{.World}} Not Found"
need_to_specify_multiple_worlds:
other: "you need to specify more than 1 world to merge"
update_available:
other: "Update available: {{.Version}} use the update command to install it."
no_update:
other: "No Updates available."
updating:
other: "Updating to {{.Version}}"
updated:
other: "Updated!"
worldname_set:
other: "worldName is now {{.Name}}"
void_generator_true:
other: "Using Void Generator"
void_generator_false:
other: "Not using Void Generator"
subchunk_before_chunk:
other: "The server didnt send the chunk before the subchunk!"
zoom_level:
other: "Zoom: {{.Level}}"
not_saving_empty:
other: "Skipping save because the world didnt contain any chunks."
saving_world:
one: "Saving world {{.Name}} with {{.Count}} Chunk"
other: "Saving world {{.Name}} with {{.Count}} Chunks"
unknown_gamerule:
other: "unknown gamerule: {{.Name}}"
adding_pack:
other: "Adding Resourcepack {{.Name}}"
using_customblocks:
other: "Using Custom Blocks"
guessing_version:
other: "couldnt determine game version, assuming > 1.18"
use_setname:
other: "use /setname <worldname>\nto set the world name"
using_under_118:
other: "using legacy (< 1.18)"
setname_desc:
other: "set user defined name for this world"
void_desc:
other: "toggle if void generator should be used"
popup_chunk_count:
one: "{{.Count}} Chunk loaded\nName: {{.Name}}"
other: "{{.Count}} Chunks loaded\nName: {{.Name}}"
warn_window_closed_not_open:
other: "Closed window that wasnt open"
saved_block_inv:
other: "Saved Block Inventory"
save_packs_with_world:
other: "save resourcepacks to the worlds"
enable_void:
other: "save with void generator"
save_image:
other: "saves an png of the map at the end"
test_block_inv:
other: "enable experimental block inventory saving"
saved:
other: "Saved: {{.Name}}"
empty_chunk:
other: "Empty Chunk."
only_with_geometry:
other: "only save skins with geometry"
name_prefix:
other: "only save players starting with this"
failed_write:
other: "failed to write {{.Part}} {{.Path}}: {{.Err}}"
packet_filter:
other: "packets to not show"
save_encrypted:
other: "save encrypted resourcepacks"
only_keys:
other: "only dump keys, dont decrypt the pack"
decrypting_packs:
other: "Decrypting Resource Packs"
no_resourcepacks:
other: "No Resourcepack sent"
writing_keys:
other: "Writing keys to {{.Path}}"
warn_key_exists:
other: "key {{.Id}} exists already"
compare_key:
other: "uuid: {{.Id}}, key in db: {{.Prev}} != new key {{.Now}}"
decrypting:
other: "Decrypting..."
list_realms_synopsis:
other: "prints all realms you have access to"
capture_synopsis:
other: "capture packets in a pcap file"
chat_log_synopsis:
other: "logs chat to a file"
debug_proxy_synopsis:
other: "verbose debug packets"
merge_synopsis:
other: "merge 2 or more worlds"
update_synopsis:
other: "self updates to latest version"
skins_proxy_synopsis:
other: "download skins from players on a server with proxy"
skins_synopsis:
other: "download skins from players on a server"
world_synopsis:
other: "download a world from a server"
pack_synopsis:
other: "download resource packs from a server"

83
locale/i18n.go Normal file
View File

@ -0,0 +1,83 @@
package locale
import (
"embed"
"flag"
"fmt"
"io"
"os"
"github.com/cloudfoundry-attic/jibber_jabber"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sirupsen/logrus"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
type Strmap map[string]interface{}
//go:embed *.yaml
var localesFS embed.FS
var lang *i18n.Localizer
func load_language(bundle *i18n.Bundle, tag language.Tag) error {
_, err := bundle.LoadMessageFileFS(localesFS, fmt.Sprintf("%s.yaml", tag.String()))
return err
}
func init() {
var defaultTag language.Tag = language.English
var err error
var languageName string
f := flag.NewFlagSet("bedrocktool", flag.ContinueOnError)
f.SetOutput(io.Discard)
f.StringVar(&languageName, "lang", "", "")
f.Parse(os.Args[1:])
// get default language
if languageName == "" {
languageName, _ = jibber_jabber.DetectLanguage()
}
defaultTag, err = language.Parse(languageName)
if err != nil {
logrus.Warn("failed to parse language name")
}
bundle := i18n.NewBundle(defaultTag)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
err = load_language(bundle, defaultTag)
if err != nil {
//logrus.Warnf("Couldnt load Language %s", languageName)
err = load_language(bundle, language.English)
if err != nil {
logrus.Error("failed to load english language")
}
}
lang = i18n.NewLocalizer(bundle, "en")
}
func Loc(id string, tmpl Strmap) string {
s, err := lang.Localize(&i18n.LocalizeConfig{
MessageID: id,
TemplateData: tmpl,
})
if err != nil {
return fmt.Sprintf("failed to translate! %s", id)
}
return s
}
func Locm(id string, tmpl Strmap, count int) string {
s, err := lang.Localize(&i18n.LocalizeConfig{
MessageID: id,
TemplateData: tmpl,
PluralCount: count,
})
if err != nil {
return fmt.Sprintf("failed to translate! %s", id)
}
return s
}

202
locale/owo.yaml Normal file
View File

@ -0,0 +1,202 @@
bedrocktool_version:
other: "Bewdwocktoo Vewsion {{.Version}}"
available_commands:
other: "Avaiwabwu Commands:"
use_to_run_command:
other: "Use 'bewdwocktoo <command>' to wun a command"
input_command:
other: "Input Command: "
fatal_error:
other: "Fatal Ewwow occuwwed."
report_issue:
other: |
if you want to weport this ewwow, pwease open an issue at
https://github.com/bedwock-tool/bewdwocktoo/issues
And attach the ewwow info, describe what you did to get this ewwow.
Thanks!
used_extra_debug_report:
other: |
NOTE: you have used extra-debug which creates a packets.log and a packets.log.gpg file
please if you want to submit an issue only upload the packets.log.gpg file.
packets.log and packets.log.gpg may include account info (never login info) so it is safer to upload the encrypted file.
that file can be read by the developer and not by people looking at the public issues page.
enter_to_exit:
other: "Press Entew to exit."
should_login_xbox:
other: "if it should login to xbox"
enable_dns:
other: "enable dns sewvew for consoles"
preload_packs:
other: "pwewoad wewesouwcepacks for pwoxy"
debug_mode:
other: "debug mode"
refreshed_token:
other: "refweshed token"
done:
other: done
starting_dns:
other: "Stawting dns at {{.Ip}}:53"
failed_to_start_dns:
other: "Failed to stawt dns sewvew: {{.Err}}"
suggest_bedrockconnect:
other: "you may have to use Bedwockconnect"
invalid_server:
other: "Invalid Sewvew Address"
enter_server:
other: "Entew Sewvew: "
disconnect:
other: "Discownect {{.Pk}}"
preloading_packs:
other: "Pwewoading Wewesouwcepacks"
failed_to_connect:
other: "failed to connect to {{.Address}}: {{.Err}}"
listening_on:
other: "Listening on {{.Address}}"
connecting:
other: "Connecting to {{.Address}}"
connected:
other: "Connected to sewvew."
connection_cancelled:
other: "connection cancelled"
failed_start_game:
other: "failed to stawt game: {{.Err}}"
help_connect:
other: "Open Minecwaft and connect to this computews local ip address to continue"
failed_to_spawn:
other: "Failed to Spawn"
not_supported_anymore:
other: "not supported anymore"
remote_address:
other: "wemote sewvew address"
ctrl_c_to_exit:
other: "Press ctwl+c to exit"
server_address_help:
other: |
accepted sewvew address formats:
123.234.123.234
123.234.123.234:19132
wealm:<Wewalmname>
wealm:<Wewalmname>:<Id>
realm_list_line:
other: "Name: {{.Name}}\tid: {{.Id}}"
failed_to_open_output:
other: "Failed to open output"
adding_world:
other: "Adding {{.World}}"
not_found:
other: "{{.World}} Not Found"
need_to_specify_multiple_worlds:
other: "you need to specify mowe than 1 world to merge"
update_available:
other: "Update avaiwabwu: {{.Version}} use the update command to install it."
no_update:
other: "No Updates avaiwabwu."
updating:
other: "Updating to {{.Version}}"
updated:
other: "Updated!"
worldname_set:
other: "wowldName is now {{.Name}}"
void_generator_true:
other: "Using Void Genewowator"
void_generator_false:
other: "Not using Void Genewowator"
subchunk_before_chunk:
other: "The sewvew didnt send the chunk befowe the subchunk!"
zoom_level:
other: "Zoom: {{.Level}}"
not_saving_empty:
other: "Skipping save because the wowld didnt contain any chunks."
saving_world:
one: "Saving wowld {{.Name}} with {{.Count}} Chunk"
other: "Saving wowld {{.Name}} with {{.Count}} Chunks"
unknown_gamerule:
other: "unknown gamerule: {{.Name}}"
adding_pack:
other: "Adding Wewesouwcepack {{.Name}}"
using_customblocks:
other: "Using Custowm Blocks"
guessing_version:
other: "couldnt detewmine game version, assuming > 1.18"
use_setname:
other: "use /setname <wowldname>\nto set the wowld name"
using_under_118:
other: "using wegacy (< 1.18)"
setname_desc:
other: "set usew defined name for this wowld"
void_desc:
other: "toggwe if void genewowator should be used"
popup_chunk_count:
one: "{{.Count}} Chunk loaded\nName: {{.Name}}"
other: "{{.Count}} Chunks loaded\nName: {{.Name}}"
warn_window_closed_not_open:
other: "Closed window that wasnt open"
saved_block_inv:
other: "Saved Bwock Inventory"
save_packs_with_world:
other: "save wewesouwcepacks to the wowlds"
enable_void:
other: "save with void genewowator"
save_image:
other: "saves an png of the map at the end"
test_block_inv:
other: "enable experimental bwock inventory saving"
saved:
other: "Saved: {{.Name}}"
empty_chunk:
other: "Empty Chunk."
only_with_geometry:
other: "only save skins with geometry"
name_prefix:
other: "only save playews stawting with this"
failed_write:
other: "failed to write {{.Part}} {{.Path}}: {{.Err}}"
packet_filter:
other: "packets to not show"
save_encrypted:
other: "save encrypted wewesouwcepacks"
only_keys:
other: "only dump keys, dont decrypt the pack"
decrypting_packs:
other: "Decrypting Wewesouwce Packs"
no_resourcepacks:
other: "No Wewesouwcepack sent"
writing_keys:
other: "Writing keys to {{.Path}}"
warn_key_exists:
other: "key {{.Id}} exists already"
compare_key:
other: "uuid: {{.Id}}, key in db: {{.Prev}} != new key {{.Now}}"
decrypting:
other: "Decrypting..."
list_realms_synopsis:
other: "pwints all wealms you have access to"
capture_synopsis:
other: "capture packets in a pcap file"
chat_log_synopsis:
other: "logs chat to a file"
debug_proxy_synopsis:
other: "vewbose debug packets"
merge_synopsis:
other: "mewge 2 or mowe wowlds"
update_synopsis:
other: "self updates to latest version"
skins_proxy_synopsis:
other: "download skins from playews on a sewvew with pwoxy"
skins_synopsis:
other: "download skins from playews on a sewvew"
world_synopsis:
other: "download a wowld from a sewvew"
pack_synopsis:
other: "download wewesouwce packs from a sewvew"

View File

@ -1,5 +0,0 @@
{
"Replace": {
"utils/resourcepack.go": "utils/resourcepack-ace.go.ignore"
}
}

106
subcommands/blind-proxy.go Normal file
View File

@ -0,0 +1,106 @@
package subcommands
import (
"context"
"flag"
"fmt"
"net"
"sync"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sandertv/go-raknet"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sirupsen/logrus"
)
type BlindProxyCMD struct {
ServerAddress string
}
func (*BlindProxyCMD) Name() string { return "blind-proxy" }
func (*BlindProxyCMD) Synopsis() string { return "raknet proxy" }
func (c *BlindProxyCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.ServerAddress, "address", "", "server address")
}
func packet_forward(src, dst *raknet.Conn) error {
for {
data, err := src.ReadPacket()
if err != nil {
return err
}
_, err = dst.Write(data)
if err != nil {
return err
}
}
}
func (c *BlindProxyCMD) Execute(ctx context.Context, ui utils.UI) error {
address, hostname, err := utils.ServerInput(ctx, c.ServerAddress)
if err != nil {
return err
}
listener, err := raknet.Listen("0.0.0.0:19132")
if err != nil {
return err
}
defer listener.Close()
logrus.Info("Listening on 0.0.0.0:19132")
listener.PongData([]byte(fmt.Sprintf("MCPE;%v;%v;%v;%v;%v;%v;Gophertunnel;%v;%v;%v;%v;",
"Proxy For "+hostname, protocol.CurrentProtocol, protocol.CurrentVersion, 0, 1,
listener.ID(), "Creative", 1, listener.Addr().(*net.UDPAddr).Port, listener.Addr().(*net.UDPAddr).Port,
)))
go func() {
<-ctx.Done()
listener.Close()
}()
clientConn, err := listener.Accept()
if err != nil {
return err
}
defer clientConn.Close()
logrus.Info("Client Connected")
serverConn, err := raknet.DialContext(ctx, address)
if err != nil {
return err
}
defer serverConn.Close()
logrus.Info("Server Connected")
logrus.Info("Forwarding Packets")
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
_err := packet_forward(clientConn.(*raknet.Conn), serverConn)
if _err != nil {
err = _err
}
}()
wg.Add(1)
go func() {
defer wg.Done()
_err := packet_forward(serverConn, clientConn.(*raknet.Conn))
if _err != nil {
err = _err
}
}()
wg.Wait()
if err != nil {
return err
}
return nil
}
func init() {
utils.RegisterCommand(&BlindProxyCMD{})
}

View File

@ -11,9 +11,9 @@ import (
"sync"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
@ -22,66 +22,67 @@ func init() {
utils.RegisterCommand(&CaptureCMD{})
}
var dump_lock sync.Mutex
var dumpLock sync.Mutex
func dump_packet(f io.WriteCloser, toServer bool, payload []byte) {
dump_lock.Lock()
defer dump_lock.Unlock()
func dumpPacket(f io.WriteCloser, toServer bool, payload []byte) {
dumpLock.Lock()
defer dumpLock.Unlock()
f.Write([]byte{0xAA, 0xAA, 0xAA, 0xAA})
packet_size := uint32(len(payload))
binary.Write(f, binary.LittleEndian, packet_size)
packetSize := uint32(len(payload))
binary.Write(f, binary.LittleEndian, packetSize)
binary.Write(f, binary.LittleEndian, toServer)
_, err := f.Write(payload)
binary.Write(f, binary.LittleEndian, time.Now().UnixMilli())
n, err := f.Write(payload)
if err != nil {
logrus.Error(err)
}
if n < int(packetSize) {
f.Write(make([]byte, int(packetSize)-n))
}
f.Write([]byte{0xBB, 0xBB, 0xBB, 0xBB})
}
type CaptureCMD struct {
server_address string
ServerAddress string
}
func (*CaptureCMD) Name() string { return "capture" }
func (*CaptureCMD) Synopsis() string { return "capture packets in a pcap file" }
func (p *CaptureCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&p.server_address, "address", "", "remote server address")
func (*CaptureCMD) Synopsis() string { return locale.Loc("capture_synopsis", nil) }
func (c *CaptureCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.ServerAddress, "address", "", "remote server address")
}
func (c *CaptureCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n" + utils.SERVER_ADDRESS_HELP
}
func (c *CaptureCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
address, hostname, err := utils.ServerInput(ctx, c.server_address)
func (c *CaptureCMD) Execute(ctx context.Context, ui utils.UI) error {
address, hostname, err := utils.ServerInput(ctx, c.ServerAddress)
if err != nil {
logrus.Fatal(err)
return 1
return err
}
fio, err := os.Create(hostname + "-" + time.Now().Format("2006-01-02_15-04-05") + ".pcap2")
os.Mkdir("captures", 0o775)
fio, err := os.Create("captures/" + hostname + "-" + time.Now().Format("2006-01-02_15-04-05") + ".pcap2")
if err != nil {
logrus.Fatal(err)
return 1
return err
}
defer fio.Close()
utils.WriteReplayHeader(fio)
proxy := utils.NewProxy(logrus.StandardLogger())
proxy, err := utils.NewProxy()
if err != nil {
logrus.Fatal(err)
}
proxy.PacketFunc = func(header packet.Header, payload []byte, src, dst net.Addr) {
from_client := src.String() == proxy.Client.LocalAddr().String()
IsfromClient := src.String() == proxy.Client.LocalAddr().String()
buf := bytes.NewBuffer(nil)
header.Write(buf)
buf.Write(payload)
dump_packet(fio, from_client, buf.Bytes())
dumpPacket(fio, IsfromClient, buf.Bytes())
}
err = proxy.Run(ctx, address)
time.Sleep(2 * time.Second)
if err != nil {
logrus.Fatal(err)
return 1
return err
}
return 0
return nil
}

View File

@ -7,49 +7,46 @@ import (
"os"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type ChatLogCMD struct {
Address string
verbose bool
ServerAddress string
Verbose bool
}
func (*ChatLogCMD) Name() string { return "chat-log" }
func (*ChatLogCMD) Synopsis() string { return "logs chat to a file" }
func (*ChatLogCMD) Synopsis() string { return locale.Loc("chat_log_synopsis", nil) }
func (c *ChatLogCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.Address, "address", "", "remote server address")
f.BoolVar(&c.verbose, "v", false, "verbose")
f.StringVar(&c.ServerAddress, "address", "", "remote server address")
f.BoolVar(&c.Verbose, "v", false, "verbose")
}
func (c *ChatLogCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n" + utils.SERVER_ADDRESS_HELP
}
func (c *ChatLogCMD) Execute(ctx context.Context, flags *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
address, hostname, err := utils.ServerInput(ctx, c.Address)
func (c *ChatLogCMD) Execute(ctx context.Context, ui utils.UI) error {
address, hostname, err := utils.ServerInput(ctx, c.ServerAddress)
if err != nil {
logrus.Error(err)
return 1
return err
}
filename := fmt.Sprintf("%s_%s_chat.log", hostname, time.Now().Format("2006-01-02_15-04-05_Z07"))
f, err := os.Create(filename)
if err != nil {
logrus.Fatal(err)
return err
}
defer f.Close()
proxy := utils.NewProxy(logrus.StandardLogger())
proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool) (packet.Packet, error) {
proxy, err := utils.NewProxy()
if err != nil {
return err
}
proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool, _ time.Time) (packet.Packet, error) {
if text, ok := pk.(*packet.Text); ok {
logLine := text.Message
if c.verbose {
if c.Verbose {
logLine += fmt.Sprintf(" (TextType: %d | XUID: %s | PlatformChatID: %s)", text.TextType, text.XUID, text.PlatformChatID)
}
f.WriteString(fmt.Sprintf("[%s] ", time.Now().Format(time.RFC3339)))
@ -62,12 +59,8 @@ func (c *ChatLogCMD) Execute(ctx context.Context, flags *flag.FlagSet, _ ...inte
return pk, nil
}
if err := proxy.Run(ctx, address); err != nil {
logrus.Error(err)
return 1
}
return 0
err = proxy.Run(ctx, address)
return err
}
func init() {

View File

@ -5,39 +5,31 @@ import (
"flag"
"strings"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/subcommands"
"github.com/sirupsen/logrus"
)
type DebugProxyCMD struct {
Address string
filter string
ServerAddress string
Filter string
}
func (*DebugProxyCMD) Name() string { return "debug-proxy" }
func (*DebugProxyCMD) Synopsis() string { return "verbose debug packets" }
func (*DebugProxyCMD) Synopsis() string { return locale.Loc("debug_proxy_synopsis", nil) }
func (c *DebugProxyCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.Address, "address", "", "remote server address")
f.StringVar(&c.filter, "filter", "", "packets to not show")
f.StringVar(&c.ServerAddress, "address", "", locale.Loc("remote_address", nil))
f.StringVar(&c.Filter, "filter", "", locale.Loc("packet_filter", nil))
}
func (c *DebugProxyCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n" + utils.SERVER_ADDRESS_HELP
}
func (c *DebugProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
address, _, err := utils.ServerInput(ctx, c.Address)
func (c *DebugProxyCMD) Execute(ctx context.Context, ui utils.UI) error {
address, _, err := utils.ServerInput(ctx, c.ServerAddress)
if err != nil {
logrus.Error(err)
return 1
return err
}
utils.G_debug = true
utils.Options.Debug = true
filters := strings.Split(c.filter, ",")
filters := strings.Split(c.Filter, ",")
if len(filters) > 0 {
for _, v := range filters {
if len(v) == 0 {
@ -52,12 +44,12 @@ func (c *DebugProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...inter
}
}
proxy := utils.NewProxy(logrus.StandardLogger())
if err := proxy.Run(ctx, address); err != nil {
logrus.Error(err)
return 1
proxy, err := utils.NewProxy()
if err != nil {
return err
}
return 0
err = proxy.Run(ctx, address)
return err
}
func init() {

View File

@ -1,3 +1,5 @@
//go:build false
package subcommands
import (
@ -7,8 +9,10 @@ import (
"os"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/df-mc/dragonfly/server/entity"
"github.com/df-mc/dragonfly/server/world/mcdb"
"github.com/df-mc/goleveldb/leveldb/opt"
"github.com/google/subcommands"
@ -22,7 +26,7 @@ type MergeCMD struct {
}
func (*MergeCMD) Name() string { return "merge" }
func (*MergeCMD) Synopsis() string { return "merge 2 or more worlds" }
func (*MergeCMD) Synopsis() string { return locale.Loc("merge_synopsis", nil) }
func (c *MergeCMD) SetFlags(f *flag.FlagSet) {
f.BoolVar(&c.legacy, "legacy", false, "if the worlds are before 1.18")
@ -34,91 +38,91 @@ func (c *MergeCMD) Usage() string {
func (c *MergeCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() == 0 {
logrus.Error("you need to specify 1 or more worlds")
logrus.Error(locale.Loc("need_to_specify_multiple_worlds", nil))
return 1
}
c.worlds = f.Args()
out_name := c.worlds[0] + "-merged"
outName := c.worlds[0] + "-merged"
prov_out, err := mcdb.New(logrus.StandardLogger(), out_name, opt.DefaultCompression)
provOut, err := mcdb.New(logrus.StandardLogger(), outName, opt.DefaultCompression)
if err != nil {
logrus.Errorf("failed to open output %s", err)
logrus.Errorf(locale.Loc("failed_to_open_output", locale.Strmap{"Err": err}))
return 1
}
for i, world_name := range c.worlds {
for i, worldName := range c.worlds {
first := i == 0
logrus.Infof("Adding %s", world_name)
s, err := os.Stat(world_name)
logrus.Infof(locale.Loc("adding_world", locale.Strmap{"World": worldName}))
s, err := os.Stat(worldName)
if errors.Is(err, os.ErrNotExist) {
logrus.Fatalf("%s not found", world_name)
logrus.Fatalf(locale.Loc("not_found", locale.Strmap{"Name": worldName}), worldName)
}
if !s.IsDir() { // if its a zip temporarily unpack it to read it
f, _ := os.Open(world_name)
world_name += "_unpack"
utils.UnpackZip(f, s.Size(), world_name)
f, _ := os.Open(worldName)
worldName += "_unpack"
utils.UnpackZip(f, s.Size(), worldName)
}
// merge it into the state
err = c.merge_worlds(prov_out, world_name, first)
err = c.mergeWorlds(provOut, worldName, first)
if err != nil {
logrus.Errorf("%s %s", world_name, err)
logrus.Errorf("%s %s", worldName, err)
return 1
}
if !s.IsDir() { // remove temp folder again
os.RemoveAll(world_name)
os.RemoveAll(worldName)
}
}
if err = prov_out.Close(); err != nil {
if err = provOut.Close(); err != nil {
logrus.Error(err)
return 1
}
time.Sleep(1 * time.Second)
if err := utils.ZipFolder(out_name+".mcworld", out_name); err != nil {
if err := utils.ZipFolder(outName+".mcworld", outName); err != nil {
logrus.Infof("zipping: %s", err)
return 1
}
os.RemoveAll(out_name)
os.RemoveAll(outName)
return 0
}
func (c *MergeCMD) merge_worlds(prov_out *mcdb.Provider, folder string, first bool) error {
prov_in, err := mcdb.New(logrus.StandardLogger(), folder, opt.DefaultCompression)
func (c *MergeCMD) mergeWorlds(provOut *mcdb.Provider, folder string, first bool) error {
provIn, err := mcdb.New(logrus.StandardLogger(), folder, opt.DefaultCompression)
if err != nil {
return err
}
count := 0
existing := prov_out.Chunks(c.legacy)
new := prov_in.Chunks(c.legacy)
existing := provOut.Chunks(c.legacy)
new := provIn.Chunks(c.legacy)
for i := range new {
if _, ok := existing[i]; !ok {
d := i.D
// chunks
ch, _, err := prov_in.LoadChunk(i.P, d)
ch, _, err := provIn.LoadChunk(i.P, d)
if err != nil {
return err
}
if err := prov_out.SaveChunk(i.P, ch, i.D); err != nil {
if err := provOut.SaveChunk(i.P, ch, i.D); err != nil {
return err
}
// blockNBT
n, err := prov_in.LoadBlockNBT(i.P, i.D)
n, err := provIn.LoadBlockNBT(i.P, i.D)
if err != nil {
return err
}
if err := prov_out.SaveBlockNBT(i.P, n, i.D); err != nil {
if err := provOut.SaveBlockNBT(i.P, n, i.D); err != nil {
return err
}
// entities
entities, err := prov_in.LoadEntities(i.P, i.D)
entities, err := provIn.LoadEntities(i.P, i.D, entity.DefaultRegistry)
if err != nil {
return err
}
if err := prov_out.SaveEntities(i.P, entities, i.D); err != nil {
if err := provOut.SaveEntities(i.P, entities, i.D); err != nil {
return err
}
count += 1
@ -127,9 +131,9 @@ func (c *MergeCMD) merge_worlds(prov_out *mcdb.Provider, folder string, first bo
if first {
logrus.Debug("Applying Settings and level.dat")
prov_out.SaveSettings(prov_in.Settings())
out_ld := prov_out.LevelDat()
copier.Copy(out_ld, prov_in.LevelDat())
provOut.SaveSettings(provIn.Settings())
outLd := provOut.LevelDat()
copier.Copy(outLd, provIn.LevelDat())
}
logrus.Infof("Added: %d", count)
return nil

View File

@ -0,0 +1,33 @@
package subcommands
import (
"context"
"flag"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sirupsen/logrus"
)
type RealmAddressCMD struct {
realm string
}
func (*RealmAddressCMD) Name() string { return "realm-address" }
func (*RealmAddressCMD) Synopsis() string { return "gets realms address" }
func (c *RealmAddressCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.realm, "realm", "", "realm name <name:id> or just name")
}
func (c *RealmAddressCMD) Execute(ctx context.Context, ui utils.UI) error {
address, _, err := utils.ServerInput(ctx, "realm:"+c.realm)
if err != nil {
return err
}
logrus.Infof("Address: %s", address)
return nil
}
func init() {
utils.RegisterCommand(&RealmAddressCMD{})
}

View File

@ -0,0 +1,36 @@
//go:build packs
package subcommands
import (
"context"
"flag"
"github.com/bedrock-tool/bedrocktool/locale"
resourcepackd "github.com/bedrock-tool/bedrocktool/subcommands/resourcepack-d"
"github.com/bedrock-tool/bedrocktool/utils"
)
type ResourcePackCMD struct {
ServerAddress string
SaveEncrypted bool
OnlyKeys bool
}
func (*ResourcePackCMD) Name() string { return "packs" }
func (*ResourcePackCMD) Synopsis() string { return locale.Loc("pack_synopsis", nil) }
func (c *ResourcePackCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.ServerAddress, "address", "", locale.Loc("remote_address", nil))
f.BoolVar(&c.SaveEncrypted, "save-encrypted", false, locale.Loc("save_encrypted", nil))
f.BoolVar(&c.OnlyKeys, "only-keys", false, locale.Loc("only_keys", nil))
}
func (c *ResourcePackCMD) Execute(ctx context.Context, ui utils.UI) error {
return resourcepackd.Execute_cmd(ctx, c.ServerAddress, c.OnlyKeys, c.SaveEncrypted)
}
func init() {
utils.RegisterCommand(&ResourcePackCMD{})
}

Binary file not shown.

View File

@ -0,0 +1,28 @@
//go:build !packs
package subcommands
import (
"context"
"errors"
"flag"
"github.com/bedrock-tool/bedrocktool/utils"
)
type ResourcePackCMD struct {
ServerAddress string
SaveEncrypted bool
OnlyKeys bool
}
func (*ResourcePackCMD) Name() string { return "packs" }
func (*ResourcePackCMD) Synopsis() string { return "NOT COMPILED" }
func (*ResourcePackCMD) SetFlags(f *flag.FlagSet) {}
func (*ResourcePackCMD) Execute(ctx context.Context, ui utils.UI) error {
return errors.New("not compiled")
}
func init() {
utils.RegisterCommand(&ResourcePackCMD{})
}

153
subcommands/skins/skin.go Normal file
View File

@ -0,0 +1,153 @@
package skins
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"image"
"image/png"
"os"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type Skin struct {
*protocol.Skin
}
type SkinGeometry struct {
Texturewidth int `json:"texturewidth"`
Textureheight int `json:"textureheight"`
VisibleBoundsWidth float64 `json:"visible_bounds_width"`
VisibleBoundsHeight float64 `json:"visible_bounds_height"`
VisibleBoundsOffset []float64 `json:"visible_bounds_offset"`
Bones []any `json:"bones"`
}
func (skin *Skin) Hash() uuid.UUID {
h := sha256.New()
h.Write(skin.SkinData)
h.Write(skin.SkinGeometry)
h.Write(skin.CapeData)
h.Write([]byte(skin.SkinID))
return uuid.NewSHA1(uuid.NameSpaceURL, h.Sum(nil))
}
func (skin *Skin) getGeometry() (*SkinGeometry, string, error) {
if !skin.HaveGeometry() {
return nil, "", errors.New("no geometry")
}
var data map[string]any
if err := json.Unmarshal(skin.SkinGeometry, &data); err != nil {
return nil, "", err
}
arr, ok := data["minecraft:geometry"].([]any)
if !ok {
return nil, "", errors.New("invalid geometry")
}
geom, ok := arr[0].(map[string]any)
if !ok {
return nil, "", errors.New("invalid geometry")
}
desc, ok := geom["description"].(map[string]any)
if !ok {
return nil, "", errors.New("invalid geometry")
}
texture_width, _ := desc["texture_width"].(float64)
texture_height, _ := desc["texture_height"].(float64)
visible_bounds_width, _ := desc["visible_bounds_width"].(float64)
visible_bounds_height, _ := desc["visible_bounds_height"].(float64)
visibleOffset, _ := desc["visible_bounds_offset"].([]float64)
return &SkinGeometry{
Texturewidth: int(texture_width),
Textureheight: int(texture_height),
VisibleBoundsWidth: visible_bounds_width,
VisibleBoundsHeight: visible_bounds_height,
VisibleBoundsOffset: visibleOffset,
Bones: geom["bones"].([]any),
}, desc["identifier"].(string), nil
}
// WriteCape writes the cape as a png at output_path
func (skin *Skin) WriteCapePng(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Cape", "Path": output_path, "Err": err}))
}
defer f.Close()
cape_tex := image.NewRGBA(image.Rect(0, 0, int(skin.CapeImageWidth), int(skin.CapeImageHeight)))
cape_tex.Pix = skin.CapeData
if err := png.Encode(f, cape_tex); err != nil {
return fmt.Errorf(locale.Loc("failed_write", locale.Strmap{"Part": "Cape", "Err": err}))
}
return nil
}
// WriteTexture writes the main texture for this skin to a file
func (skin *Skin) writeSkinTexturePng(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Meta", "Path": output_path, "Err": err}))
}
defer f.Close()
skin_tex := image.NewRGBA(image.Rect(0, 0, int(skin.SkinImageWidth), int(skin.SkinImageHeight)))
skin_tex.Pix = skin.SkinData
if err := png.Encode(f, skin_tex); err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Texture", "Path": output_path, "Err": err}))
}
return nil
}
func (skin *Skin) writeMetadataJson(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Meta", "Path": output_path, "Err": err}))
}
defer f.Close()
d, err := json.MarshalIndent(SkinMeta{
skin.SkinID,
skin.PlayFabID,
skin.PremiumSkin,
skin.PersonaSkin,
skin.CapeID,
skin.SkinColour,
skin.ArmSize,
skin.Trusted,
skin.PersonaPieces,
}, "", " ")
if err != nil {
return err
}
f.Write(d)
return nil
}
func (skin *Skin) HaveGeometry() bool {
return len(skin.SkinGeometry) > 0
}
func (skin *Skin) HaveCape() bool {
return len(skin.CapeData) > 0
}
func (skin *Skin) HaveAnimations() bool {
return len(skin.Animations) > 0
}
func (skin *Skin) HaveTint() bool {
return len(skin.PieceTintColours) > 0
}
func (skin *Skin) Complex() bool {
return skin.HaveGeometry() || skin.HaveCape() || skin.HaveAnimations() || skin.HaveTint()
}

View File

@ -0,0 +1,150 @@
package skins
import (
"encoding/json"
"fmt"
"os"
"path"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
)
type _skinWithIndex struct {
i int
skin *Skin
}
func (s _skinWithIndex) Name(name string) string {
return fmt.Sprintf("%s-%d", name, s.i)
}
type SkinPack struct {
skins map[uuid.UUID]_skinWithIndex
Name string
}
type skinEntry struct {
LocalizationName string `json:"localization_name"`
Geometry string `json:"geometry"`
Texture string `json:"texture"`
Type string `json:"type"`
}
func NewSkinPack(name, fpath string) *SkinPack {
return &SkinPack{
skins: make(map[uuid.UUID]_skinWithIndex),
Name: name,
}
}
func (s *SkinPack) AddSkin(skin *Skin) bool {
sh := skin.Hash()
if _, ok := s.skins[sh]; !ok {
s.skins[sh] = _skinWithIndex{len(s.skins) + 1, skin}
return true
}
return false
}
func (s *SkinPack) Save(fpath, serverName string) error {
os.MkdirAll(fpath, 0o755)
var skinsJson struct {
Skins []skinEntry `json:"skins"`
}
geometryJson := map[string]SkinGeometry{}
for _, s2 := range s.skins { // write skin texture
skinName := s2.Name(s.Name)
if err := s2.skin.writeSkinTexturePng(path.Join(fpath, skinName+".png")); err != nil {
return err
}
if err := s2.skin.writeMetadataJson(path.Join(fpath, skinName+"_metadata.json")); err != nil {
return err
}
if s2.skin.HaveCape() {
if err := s2.skin.WriteCapePng(path.Join(fpath, skinName+"_cape.png")); err != nil {
return err
}
}
entry := skinEntry{
LocalizationName: skinName,
Texture: skinName + ".png",
Type: "free",
}
if s2.skin.ArmSize == "wide" {
entry.Geometry = "minecraft.geometry.steve"
} else {
entry.Geometry = "minecraft.geometry.alex"
}
if s2.skin.HaveGeometry() {
geometry, geometryName, err := s2.skin.getGeometry()
if err != nil {
logrus.Warnf("failed to decode geometry %s", skinName)
} else {
geometryJson[geometryName] = *geometry
entry.Geometry = geometryName
}
}
skinsJson.Skins = append(skinsJson.Skins, entry)
}
if len(geometryJson) > 0 { // geometry.json
f, err := os.Create(path.Join(fpath, "geometry.json"))
if err != nil {
return err
}
e := json.NewEncoder(f)
e.SetIndent("", "\t")
if err := e.Encode(geometryJson); err != nil {
return err
}
}
{ // skins.json
f, err := os.Create(path.Join(fpath, "skins.json"))
if err != nil {
return err
}
e := json.NewEncoder(f)
e.SetIndent("", "\t")
if err := e.Encode(skinsJson); err != nil {
return err
}
}
{ // manifest.json
manifest := resource.Manifest{
FormatVersion: 2,
Header: resource.Header{
Name: s.Name,
Description: serverName + " " + s.Name,
UUID: uuid.NewString(),
Version: [3]int{1, 0, 0},
MinimumGameVersion: [3]int{1, 17, 0},
},
Modules: []resource.Module{
{
UUID: uuid.NewString(),
Description: s.Name + " Skinpack",
Type: "skin_pack",
Version: [3]int{1, 0, 0},
},
},
}
if err := utils.WriteManifest(&manifest, fpath); err != nil {
return err
}
}
return nil
}

View File

@ -1,61 +0,0 @@
package skins
import (
"context"
"flag"
"fmt"
"os"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type SkinProxyCMD struct {
server_address string
filter string
only_with_geometry bool
}
func (*SkinProxyCMD) Name() string { return "skins-proxy" }
func (*SkinProxyCMD) Synopsis() string { return "download skins from players on a server with proxy" }
func (c *SkinProxyCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.server_address, "address", "", "remote server address")
f.StringVar(&c.filter, "filter", "", "player name filter prefix")
f.BoolVar(&c.only_with_geometry, "only-geom", false, "only save skins with geometry")
}
func (c *SkinProxyCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n" + utils.SERVER_ADDRESS_HELP
}
func (c *SkinProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
address, hostname, err := utils.ServerInput(ctx, c.server_address)
if err != nil {
logrus.Error(err)
return 1
}
out_path := fmt.Sprintf("skins/%s", hostname)
os.MkdirAll(out_path, 0o755)
proxy := utils.NewProxy(logrus.StandardLogger())
proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool) (packet.Packet, error) {
if !toServer {
process_packet_skins(proxy.Client, out_path, pk, c.filter, c.only_with_geometry)
}
return pk, nil
}
if err := proxy.Run(ctx, address); err != nil {
logrus.Error(err)
return 1
}
return 0
}
func init() {
utils.RegisterCommand(&SkinProxyCMD{})
}

View File

@ -1,32 +1,24 @@
package skins
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"image"
"image/png"
"io"
"os"
"path"
"strings"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/flytam/filenamify"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type Skin struct {
protocol.Skin
}
type SkinMeta struct {
SkinID string
PlayFabID string
@ -39,267 +31,159 @@ type SkinMeta struct {
PersonaPieces []protocol.PersonaPiece
}
// WriteGeometry writes the geometry json for the skin to output_path
func (skin *Skin) WriteGeometry(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return fmt.Errorf("failed to write Geometry %s: %s", output_path, err)
}
defer f.Close()
io.Copy(f, bytes.NewReader(skin.SkinGeometry))
return nil
type skinsSession struct {
PlayerNameFilter string
OnlyIfHasGeometry bool
ServerName string
Proxy *utils.ProxyContext
fpath string
playerSkinPacks map[uuid.UUID]*SkinPack
playerNames map[uuid.UUID]string
}
// WriteCape writes the cape as a png at output_path
func (skin *Skin) WriteCape(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return fmt.Errorf("failed to write Cape %s: %s", output_path, err)
}
defer f.Close()
cape_tex := image.NewRGBA(image.Rect(0, 0, int(skin.CapeImageWidth), int(skin.CapeImageHeight)))
cape_tex.Pix = skin.CapeData
func NewSkinsSession(proxy *utils.ProxyContext, serverName, fpath string) *skinsSession {
return &skinsSession{
ServerName: serverName,
Proxy: proxy,
fpath: fpath,
if err := png.Encode(f, cape_tex); err != nil {
return fmt.Errorf("error writing skin: %s", err)
playerSkinPacks: make(map[uuid.UUID]*SkinPack),
playerNames: make(map[uuid.UUID]string),
}
return nil
}
// WriteAnimations writes skin animations to the folder
func (skin *Skin) WriteAnimations(output_path string) error {
logrus.Warnf("%s has animations (unimplemented)", output_path)
return nil
func (s *skinsSession) AddPlayerSkin(playerID uuid.UUID, playerName string, skin *Skin) (added bool) {
p, ok := s.playerSkinPacks[playerID]
if !ok {
creating := fmt.Sprintf("Creating Skinpack for %s", playerName)
s.Proxy.SendPopup(creating)
logrus.Info(creating)
p = NewSkinPack(playerName, s.fpath)
s.playerSkinPacks[playerID] = p
}
if p.AddSkin(skin) {
if ok {
addedStr := fmt.Sprintf("Added a skin to %s", playerName)
s.Proxy.SendPopup(addedStr)
logrus.Info(addedStr)
}
added = true
}
if err := p.Save(path.Join(s.fpath, playerName), s.ServerName); err != nil {
logrus.Error(err)
}
return added
}
// WriteTexture writes the main texture for this skin to a file
func (skin *Skin) WriteTexture(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return fmt.Errorf("error writing Texture: %s", err)
}
defer f.Close()
skin_tex := image.NewRGBA(image.Rect(0, 0, int(skin.SkinImageWidth), int(skin.SkinImageHeight)))
skin_tex.Pix = skin.SkinData
if err := png.Encode(f, skin_tex); err != nil {
return fmt.Errorf("error writing Texture: %s", err)
}
return nil
}
func (skin *Skin) WriteTint(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return fmt.Errorf("failed to write Tint %s: %s", output_path, err)
}
defer f.Close()
err = json.NewEncoder(f).Encode(skin.PieceTintColours)
if err != nil {
return fmt.Errorf("failed to write Tint %s: %s", output_path, err)
}
return nil
}
func (skin *Skin) WriteMeta(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return fmt.Errorf("failed to write Tint %s: %s", output_path, err)
}
defer f.Close()
d, err := json.MarshalIndent(SkinMeta{
skin.SkinID,
skin.PlayFabID,
skin.PremiumSkin,
skin.PersonaSkin,
skin.CapeID,
skin.SkinColour,
skin.ArmSize,
skin.Trusted,
skin.PersonaPieces,
}, "", " ")
if err != nil {
return err
}
f.Write(d)
return nil
}
func (skin *Skin) Complex() bool {
have_geometry, have_cape, have_animations, have_tint := len(skin.SkinGeometry) > 0, len(skin.CapeData) > 0, len(skin.Animations) > 0, len(skin.PieceTintColours) > 0
return have_geometry || have_cape || have_animations || have_tint
}
// Write writes all data for this skin to a folder
func (skin *Skin) Write(output_path, name string) error {
name, _ = filenamify.FilenamifyV2(name)
skin_dir := path.Join(output_path, name)
have_geometry, have_cape, have_animations, have_tint := len(skin.SkinGeometry) > 0, len(skin.CapeData) > 0, len(skin.Animations) > 0, len(skin.PieceTintColours) > 0
os.MkdirAll(skin_dir, 0o755)
if have_geometry {
if err := skin.WriteGeometry(path.Join(skin_dir, "geometry.json")); err != nil {
return err
func (s *skinsSession) AddSkin(playerName string, playerID uuid.UUID, playerSkin *protocol.Skin) (string, *Skin, bool) {
if playerName == "" {
playerName = s.playerNames[playerID]
if playerName == "" {
playerName = playerID.String()
}
}
if have_cape {
if err := skin.WriteCape(path.Join(skin_dir, "cape.png")); err != nil {
return err
}
}
if have_animations {
if err := skin.WriteAnimations(skin_dir); err != nil {
return err
}
}
if have_tint {
if err := skin.WriteTint(path.Join(skin_dir, "tint.json")); err != nil {
return err
}
if !strings.HasPrefix(playerName, s.PlayerNameFilter) {
return "", nil, false
}
s.playerNames[playerID] = playerName
if err := skin.WriteMeta(path.Join(skin_dir, "metadata.json")); err != nil {
return err
skin := &Skin{playerSkin}
if s.OnlyIfHasGeometry && !skin.HaveGeometry() {
return "", nil, false
}
wasAdded := s.AddPlayerSkin(playerID, playerName, skin)
return skin.WriteTexture(skin_dir + "/skin.png")
return playerName, skin, wasAdded
}
// puts the skin at output_path if the filter matches it
// internally converts the struct so it can use the extra methods
func write_skin(output_path, name string, skin *protocol.Skin) {
logrus.Infof("Writing skin for %s\n", name)
_skin := &Skin{*skin}
if err := _skin.Write(output_path, name); err != nil {
fmt.Fprintf(os.Stderr, "Error writing skin: %s\n", err)
}
type skinAdd struct {
PlayerName string
Skin *protocol.Skin
}
var (
skin_players = make(map[string]string)
skin_player_counts = make(map[string]int)
)
func popup_skin_saved(conn *minecraft.Conn, name string) {
if conn != nil {
(&utils.ProxyContext{Client: conn}).SendPopup(fmt.Sprintf("%s Skin was Saved", name))
}
}
func skin_meta_get_skinid(path string) string {
cont, err := os.ReadFile(fmt.Sprintf("%s/metadata.json", path))
if err != nil {
return ""
}
var meta SkinMeta
if err := json.Unmarshal(cont, &meta); err != nil {
return ""
}
return meta.SkinID
}
func save_player_skin(conn *minecraft.Conn, out_path, player_name string, skin *protocol.Skin) {
count := skin_player_counts[player_name]
if count > 0 {
meta_id := skin_meta_get_skinid(fmt.Sprintf("%s/%s_%d", out_path, player_name, count-1))
if meta_id == skin.SkinID {
return // skin same as before
}
}
skin_player_counts[player_name]++
count++
write_skin(out_path, fmt.Sprintf("%s_%d", player_name, count), skin)
popup_skin_saved(conn, player_name)
}
func process_packet_skins(conn *minecraft.Conn, out_path string, pk packet.Packet, filter string, only_if_geom bool) {
switch _pk := pk.(type) {
case *packet.PlayerSkin:
player_name := skin_players[_pk.UUID.String()]
if player_name == "" {
player_name = _pk.UUID.String()
}
if !strings.HasPrefix(player_name, filter) {
return
}
if only_if_geom && len(_pk.Skin.SkinGeometry) == 0 {
return
}
save_player_skin(conn, out_path, player_name, &_pk.Skin)
func (s *skinsSession) ProcessPacket(pk packet.Packet) (out []skinAdd) {
switch pk := pk.(type) {
case *packet.PlayerList:
if _pk.ActionType == 1 { // remove
return
if pk.ActionType == 1 { // remove
return nil
}
for _, player := range _pk.Entries {
player_name := utils.CleanupName(player.Username)
if player_name == "" {
player_name = player.UUID.String()
for _, player := range pk.Entries {
playerName, skin, wasAdded := s.AddSkin(utils.CleanupName(player.Username), player.UUID, &player.Skin)
if wasAdded {
out = append(out, skinAdd{
PlayerName: playerName,
Skin: skin.Skin,
})
}
if !strings.HasPrefix(player_name, filter) {
return
}
if only_if_geom && len(player.Skin.SkinGeometry) == 0 {
return
}
skin_players[player.UUID.String()] = player_name
save_player_skin(conn, out_path, player_name, &player.Skin)
}
case *packet.AddPlayer:
if _, ok := s.playerNames[pk.UUID]; !ok {
s.playerNames[pk.UUID] = utils.CleanupName(pk.Username)
}
}
return out
}
type SkinCMD struct {
server_address string
filter string
ServerAddress string
Filter string
NoProxy bool
}
func (*SkinCMD) Name() string { return "skins" }
func (*SkinCMD) Synopsis() string { return "download all skins from players on a server" }
func (*SkinCMD) Synopsis() string { return locale.Loc("skins_synopsis", nil) }
func (c *SkinCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.server_address, "address", "", "remote server address")
f.StringVar(&c.filter, "filter", "", "player name filter prefix")
f.StringVar(&c.ServerAddress, "address", "", locale.Loc("remote_address", nil))
f.StringVar(&c.Filter, "filter", "", locale.Loc("name_prefix", nil))
f.BoolVar(&c.NoProxy, "no-proxy", false, "use headless version")
}
func (c *SkinCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n"
}
func (c *SkinCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
address, hostname, err := utils.ServerInput(ctx, c.server_address)
func (c *SkinCMD) Execute(ctx context.Context, ui utils.UI) error {
address, hostname, err := utils.ServerInput(ctx, c.ServerAddress)
if err != nil {
fmt.Fprint(os.Stderr, err)
return 1
return err
}
serverConn, err := utils.ConnectServer(ctx, address, nil, false, nil)
if err != nil {
fmt.Fprint(os.Stderr, err)
return 1
proxy, _ := utils.NewProxy()
proxy.WithClient = !c.NoProxy
proxy.OnClientConnect = func(proxy *utils.ProxyContext, hasClient bool) {
ui.Message(messages.SetUIState, messages.UIStateConnecting)
}
defer serverConn.Close()
out_path := fmt.Sprintf("skins/%s", hostname)
if err := serverConn.DoSpawnContext(ctx); err != nil {
fmt.Fprint(os.Stderr, err)
return 1
}
logrus.Info("Connected")
logrus.Info("Press ctrl+c to exit")
os.MkdirAll(out_path, 0o755)
for {
pk, err := serverConn.ReadPacket()
proxy.ConnectCB = func(proxy *utils.ProxyContext, err error) bool {
if err != nil {
return 1
return false
}
process_packet_skins(nil, out_path, pk, c.filter, false)
ui.Message(messages.SetUIState, messages.UIStateMain)
logrus.Info(locale.Loc("ctrl_c_to_exit", nil))
return true
}
outPathBase := fmt.Sprintf("skins/%s", hostname)
os.MkdirAll(outPathBase, 0o755)
s := NewSkinsSession(proxy, hostname, outPathBase)
proxy.PacketCB = func(pk packet.Packet, _ *utils.ProxyContext, toServer bool, _ time.Time) (packet.Packet, error) {
if !toServer {
for _, s := range s.ProcessPacket(pk) {
ui.Message(messages.NewSkin, messages.NewSkinPayload{
PlayerName: s.PlayerName,
Skin: s.Skin,
})
}
}
return pk, nil
}
if proxy.WithClient {
ui.Message(messages.SetUIState, messages.UIStateConnect)
} else {
ui.Message(messages.SetUIState, messages.UIStateConnecting)
}
err = proxy.Run(ctx, address)
return err
}
func init() {

View File

@ -1,45 +1,38 @@
// Package subcommands ...
package subcommands
import (
"context"
"flag"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sirupsen/logrus"
"github.com/google/subcommands"
)
type UpdateCMD struct{}
func (*UpdateCMD) Name() string { return "update" }
func (*UpdateCMD) Synopsis() string { return "self updates to latest version" }
func (*UpdateCMD) Name() string { return "update" }
func (*UpdateCMD) Synopsis() string { return locale.Loc("update_synopsis", nil) }
func (c *UpdateCMD) SetFlags(f *flag.FlagSet) {}
func (c *UpdateCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n"
}
func (c *UpdateCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
func (c *UpdateCMD) Execute(ctx context.Context, ui utils.UI) error {
newVersion, err := utils.Updater.UpdateAvailable()
if err != nil {
logrus.Error(err)
return 1
return err
}
if newVersion == "" {
logrus.Info("No Updates available.")
return 0
logrus.Info(locale.Loc("no_update", nil))
return nil
}
logrus.Infof("Updating to %s", newVersion)
logrus.Infof(locale.Loc("updating", locale.Strmap{"Version": newVersion}))
if err := utils.Updater.Update(); err != nil {
logrus.Error(err)
return 1
return err
}
logrus.Infof("Updated!")
return 0
logrus.Infof(locale.Loc("updated", nil))
return nil
}
func init() {

126
subcommands/world/chunk.go Normal file
View File

@ -0,0 +1,126 @@
package world
import (
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
func (w *WorldState) processChangeDimension(pk *packet.ChangeDimension) {
if len(w.chunks) > 0 {
w.SaveAndReset()
} else {
logrus.Info(locale.Loc("not_saving_empty", nil))
w.Reset()
}
dimensionID := pk.Dimension
if w.ispre118 {
dimensionID += 10
}
w.Dim = dimensionIDMap[uint8(dimensionID)]
}
func (w *WorldState) processLevelChunk(pk *packet.LevelChunk) {
_, exists := w.chunks[pk.Position]
if exists {
return
}
// ignore empty chunks THANKS WEIRD SERVER SOFTWARE DEVS
if len(pk.RawPayload) == 0 {
logrus.Info(locale.Loc("empty_chunk", nil))
return
}
ch, blockNBTs, err := chunk.NetworkDecode(world.AirRID(), pk.RawPayload, int(pk.SubChunkCount), w.Dim.Range(), w.ispre118, w.bp.HasBlocks())
if err != nil {
logrus.Error(err)
return
}
if blockNBTs != nil {
w.blockNBT[protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
}] = blockNBTs
}
// check if chunk is empty
empty := true
for _, sub := range ch.Sub() {
if !sub.Empty() {
empty = false
break
}
}
w.chunks[pk.Position] = ch
if pk.SubChunkRequestMode == protocol.SubChunkRequestModeLegacy {
if !empty {
w.mapUI.SetChunk(pk.Position, ch)
}
} else {
// request all the subchunks
max := w.Dim.Range().Height() / 16
if pk.SubChunkRequestMode == protocol.SubChunkRequestModeLimited {
max = int(pk.HighestSubChunk)
}
w.proxy.Server.WritePacket(&packet.SubChunkRequest{
Dimension: int32(w.Dim.EncodeDimension()),
Position: protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
},
Offsets: offsetTable[:max],
})
}
}
func (w *WorldState) processSubChunk(pk *packet.SubChunk) {
posToRedraw := make(map[protocol.ChunkPos]bool)
for _, sub := range pk.SubChunkEntries {
var (
absX = pk.Position[0] + int32(sub.Offset[0])
absY = pk.Position[1] + int32(sub.Offset[1])
absZ = pk.Position[2] + int32(sub.Offset[2])
subPos = protocol.SubChunkPos{absX, absY, absZ}
pos = protocol.ChunkPos{absX, absZ}
)
ch, ok := w.chunks[pos]
if !ok {
logrus.Error(locale.Loc("subchunk_before_chunk", nil))
continue
}
blockNBT, err := ch.ApplySubChunkEntry(uint8(absY), &sub)
if err != nil {
logrus.Error(err)
}
if blockNBT != nil {
w.blockNBT[subPos] = blockNBT
}
posToRedraw[pos] = true
}
// redraw the chunks
for pos := range posToRedraw {
w.mapUI.SetChunk(pos, w.chunks[pos])
}
w.mapUI.SchedRedraw()
}
func (w *WorldState) ProcessChunkPackets(pk packet.Packet) packet.Packet {
switch pk := pk.(type) {
case *packet.ChangeDimension:
w.processChangeDimension(pk)
case *packet.LevelChunk:
w.processLevelChunk(pk)
w.proxy.SendPopup(locale.Locm("popup_chunk_count", locale.Strmap{"Count": len(w.chunks), "Name": w.WorldName}, len(w.chunks)))
case *packet.SubChunk:
w.processSubChunk(pk)
}
return pk
}

View File

@ -20,6 +20,9 @@ func blockColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) (blockColor color.R
} else {
b, found := world.BlockByRuntimeID(rid)
if found {
if d, ok := b.(block.LightDiffuser); ok && d.LightDiffusionLevel() == 0 && y > int16(c.Range().Min()) {
return blockColorAt(c, x, y-1, z)
}
if _, ok := b.(block.Water); ok {
y2 := c.HeightMap().At(x, z)
depth := y - y2
@ -50,18 +53,18 @@ func blockColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) (blockColor color.R
func chunkGetColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) color.RGBA {
p := cube.Pos{int(x), int(y), int(z)}
have_up := false
haveUp := false
p.Side(cube.FaceUp).Neighbours(func(neighbour cube.Pos) {
if neighbour.X() < 0 || neighbour.X() >= 16 || neighbour.Z() < 0 || neighbour.Z() >= 16 || neighbour.Y() > c.Range().Max() {
return
}
if !have_up {
block_rid := c.Block(uint8(neighbour[0]), int16(neighbour[1]), uint8(neighbour[2]), 0)
if block_rid > 0 {
b, found := world.BlockByRuntimeID(block_rid)
if !haveUp {
blockRid := c.Block(uint8(neighbour[0]), int16(neighbour[1]), uint8(neighbour[2]), 0)
if blockRid > 0 {
b, found := world.BlockByRuntimeID(blockRid)
if found {
if _, ok := b.(block.Air); !ok {
have_up = true
haveUp = true
}
}
}
@ -70,7 +73,7 @@ func chunkGetColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) color.RGBA {
col := blockColorAt(c, x, y, z)
if have_up {
if haveUp {
if col.R > 10 {
col.R -= 10
}

View File

@ -1,18 +1,19 @@
package world
package world_test
import (
"image/png"
"os"
"testing"
"github.com/bedrock-tool/bedrocktool/subcommands/world"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world/chunk"
)
func Test(t *testing.T) {
data, _ := os.ReadFile("chunk.bin")
ch, _, _ := chunk.NetworkDecode(33, data, 6, cube.Range{0, 255}, true)
i := Chunk2Img(ch)
ch, _, _ := chunk.NetworkDecode(33, data, 6, cube.Range{0, 255}, true, false)
i := world.Chunk2Img(ch)
f, _ := os.Create("chunk.png")
png.Encode(f, i)
f.Close()
@ -21,7 +22,7 @@ func Test(t *testing.T) {
func Benchmark_chunk_decode(b *testing.B) {
data, _ := os.ReadFile("chunk.bin")
for i := 0; i < b.N; i++ {
_, _, err := chunk.NetworkDecode(33, data, 6, cube.Range{0, 255}, true)
_, _, err := chunk.NetworkDecode(33, data, 6, cube.Range{0, 255}, true, false)
if err != nil {
b.Error(err)
}
@ -30,9 +31,9 @@ func Benchmark_chunk_decode(b *testing.B) {
func Benchmark_render_chunk(b *testing.B) {
data, _ := os.ReadFile("chunk.bin")
ch, _, _ := chunk.NetworkDecode(33, data, 6, cube.Range{0, 255}, true)
ch, _, _ := chunk.NetworkDecode(33, data, 6, cube.Range{0, 255}, true, false)
for i := 0; i < b.N; i++ {
Chunk2Img(ch)
world.Chunk2Img(ch)
}
}

185
subcommands/world/entity.go Normal file
View File

@ -0,0 +1,185 @@
package world
import (
"github.com/bedrock-tool/bedrocktool/utils/behaviourpack"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl32"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)
type entityState struct {
RuntimeID uint64
UniqueID int64
EntityType string
Position mgl32.Vec3
Pitch, Yaw float32
HeadYaw, BodyYaw float32
Velocity mgl32.Vec3
Metadata protocol.EntityMetadata
}
type serverEntityType struct {
world.SaveableEntityType
Encoded string
NBT map[string]any
}
func (t serverEntityType) EncodeEntity() string {
return t.Encoded
}
func (t serverEntityType) BBox(e world.Entity) cube.BBox {
return cube.Box(-0.5, 0, -0.5, 0.5, 1, 0.5)
}
func (t serverEntityType) DecodeNBT(m map[string]any) world.Entity {
return nil // not implemented, and never should
}
func (t serverEntityType) EncodeNBT(e world.Entity) map[string]any {
return t.NBT
}
func (t serverEntityType) UniqueID() int64 {
return t.NBT["UniqueID"].(int64)
}
type serverEntity struct {
world.Entity
EntityType serverEntityType
}
func (e serverEntity) Type() world.EntityType {
return e.EntityType
}
func (w *WorldState) processAddActor(pk *packet.AddActor) {
e, ok := w.entities[pk.EntityRuntimeID]
if !ok {
e = &entityState{
RuntimeID: pk.EntityRuntimeID,
UniqueID: pk.EntityUniqueID,
EntityType: pk.EntityType,
Metadata: make(map[uint32]any),
}
w.entities[pk.EntityRuntimeID] = e
w.bp.AddEntity(behaviourpack.EntityIn{
Identifier: pk.EntityType,
Attr: pk.Attributes,
Meta: pk.EntityMetadata,
})
}
e.Position = pk.Position
e.Pitch = pk.Pitch
e.Yaw = pk.Yaw
e.BodyYaw = pk.BodyYaw
e.HeadYaw = pk.HeadYaw
e.Velocity = pk.Velocity
for k, v := range pk.EntityMetadata {
e.Metadata[k] = v
}
}
func entityMetadataToNBT(metadata protocol.EntityMetadata, nbt map[string]any) {
if variant, ok := metadata[protocol.EntityDataKeyVariant]; ok {
nbt["Variant"] = variant
}
if markVariant, ok := metadata[protocol.EntityDataKeyMarkVariant]; ok {
nbt["MarkVariant"] = markVariant
}
if color, ok := metadata[protocol.EntityDataKeyColorIndex]; ok {
nbt["Color"] = color
}
if color2, ok := metadata[protocol.EntityDataKeyColorTwoIndex]; ok {
nbt["Color2"] = color2
}
if name, ok := metadata[protocol.EntityDataKeyName]; ok {
nbt["CustomName"] = name
}
if ShowNameTag, ok := metadata[protocol.EntityDataKeyAlwaysShowNameTag]; ok {
if ShowNameTag != 0 {
nbt["CustomNameVisible"] = true
} else {
nbt["CustomNameVisible"] = false
}
}
}
func vec3float32(x mgl32.Vec3) []float32 {
return []float32{float32(x[0]), float32(x[1]), float32(x[2])}
}
func (s *entityState) ToServerEntity() serverEntity {
e := serverEntity{
EntityType: serverEntityType{
Encoded: s.EntityType,
NBT: map[string]any{
"Pos": vec3float32(s.Position),
"Rotation": []float32{s.Yaw, s.Pitch},
"Motion": vec3float32(s.Velocity),
"UniqueID": int64(s.UniqueID),
},
},
}
entityMetadataToNBT(s.Metadata, e.EntityType.NBT)
return e
}
func (w *WorldState) ProcessEntityPackets(pk packet.Packet) packet.Packet {
switch pk := pk.(type) {
case *packet.AddActor:
w.processAddActor(pk)
case *packet.RemoveActor:
delete(w.entities, uint64(pk.EntityUniqueID))
case *packet.SetActorData:
e, ok := w.entities[pk.EntityRuntimeID]
if ok {
for k, v := range pk.EntityMetadata {
e.Metadata[k] = v
}
}
case *packet.SetActorMotion:
e, ok := w.entities[pk.EntityRuntimeID]
if ok {
e.Velocity = pk.Velocity
}
case *packet.MoveActorDelta:
e, ok := w.entities[pk.EntityRuntimeID]
if ok {
if pk.Flags&packet.MoveActorDeltaFlagHasX != 0 {
e.Position[0] = pk.Position[0]
}
if pk.Flags&packet.MoveActorDeltaFlagHasY != 0 {
e.Position[1] = pk.Position[1]
}
if pk.Flags&packet.MoveActorDeltaFlagHasZ != 0 {
e.Position[2] = pk.Position[2]
}
if pk.Flags&packet.MoveActorDeltaFlagHasRotX != 0 {
e.Pitch = pk.Rotation.X()
}
if pk.Flags&packet.MoveActorDeltaFlagHasRotY != 0 {
e.Yaw = pk.Rotation.Y()
}
//if pk.Flags&packet.MoveActorDeltaFlagHasRotZ != 0 {
// no roll
//}
}
case *packet.MoveActorAbsolute:
e, ok := w.entities[pk.EntityRuntimeID]
if ok {
e.Position = pk.Position
e.Pitch = pk.Rotation.X()
e.Yaw = pk.Rotation.Y()
}
}
return pk
}

167
subcommands/world/items.go Normal file
View File

@ -0,0 +1,167 @@
package world
import (
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils/nbtconv"
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type itemContainer struct {
OpenPacket *packet.ContainerOpen
Content *packet.InventoryContent
}
func (w *WorldState) processItemPacketsServer(pk packet.Packet) packet.Packet {
switch pk := pk.(type) {
case *packet.ContainerOpen:
if w.experimentInventory {
// add to open containers
existing, ok := w.openItemContainers[pk.WindowID]
if !ok {
existing = &itemContainer{}
}
w.openItemContainers[pk.WindowID] = &itemContainer{
OpenPacket: pk,
Content: existing.Content,
}
}
case *packet.InventoryContent:
if w.experimentInventory {
// save content
existing, ok := w.openItemContainers[byte(pk.WindowID)]
if !ok {
if pk.WindowID == 0x0 { // inventory
w.openItemContainers[byte(pk.WindowID)] = &itemContainer{
Content: pk,
}
}
break
}
existing.Content = pk
}
case *packet.ContainerClose:
if w.experimentInventory {
switch pk.WindowID {
case protocol.WindowIDArmour: // todo handle
case protocol.WindowIDOffHand: // todo handle
case protocol.WindowIDUI:
case protocol.WindowIDInventory: // todo handle
default:
// find container info
existing, ok := w.openItemContainers[byte(pk.WindowID)]
if !ok {
logrus.Warn(locale.Loc("warn_window_closed_not_open", nil))
break
}
if existing.Content == nil {
break
}
pos := existing.OpenPacket.ContainerPosition
cp := protocol.SubChunkPos{pos.X() << 4, pos.Z() << 4}
// create inventory
inv := inventory.New(len(existing.Content.Content), nil)
for i, c := range existing.Content.Content {
item := stackToItem(c.Stack)
inv.SetItem(i, item)
}
// put into subchunk
nbts := w.blockNBT[cp]
for i, v := range nbts {
NBTPos := protocol.BlockPos{v["x"].(int32), v["y"].(int32), v["z"].(int32)}
if NBTPos == pos {
w.blockNBT[cp][i]["Items"] = nbtconv.InvToNBT(inv)
}
}
w.proxy.SendMessage(locale.Loc("saved_block_inv", nil))
// remove it again
delete(w.openItemContainers, byte(pk.WindowID))
}
}
case *packet.ItemComponent:
w.bp.ApplyComponentEntries(pk.Items)
}
return pk
}
func (w *WorldState) processItemPacketsClient(pk packet.Packet, forward *bool) packet.Packet {
switch pk := pk.(type) {
case *packet.ItemStackRequest:
var requests []protocol.ItemStackRequest
for _, isr := range pk.Requests {
for _, sra := range isr.Actions {
if sra, ok := sra.(*protocol.TakeStackRequestAction); ok {
if sra.Source.StackNetworkID == MapItemPacket.Content[0].StackNetworkID {
continue
}
}
if sra, ok := sra.(*protocol.DropStackRequestAction); ok {
if sra.Source.StackNetworkID == MapItemPacket.Content[0].StackNetworkID {
continue
}
}
if sra, ok := sra.(*protocol.DestroyStackRequestAction); ok {
if sra.Source.StackNetworkID == MapItemPacket.Content[0].StackNetworkID {
continue
}
}
if sra, ok := sra.(*protocol.PlaceInContainerStackRequestAction); ok {
if sra.Source.StackNetworkID == MapItemPacket.Content[0].StackNetworkID {
continue
}
}
if sra, ok := sra.(*protocol.TakeOutContainerStackRequestAction); ok {
if sra.Source.StackNetworkID == MapItemPacket.Content[0].StackNetworkID {
continue
}
}
if sra, ok := sra.(*protocol.DestroyStackRequestAction); ok {
if sra.Source.StackNetworkID == MapItemPacket.Content[0].StackNetworkID {
continue
}
}
}
requests = append(requests, isr)
}
pk.Requests = requests
case *packet.MobEquipment:
if pk.NewItem.Stack.NBTData["map_uuid"] == int64(ViewMapID) {
*forward = false
}
}
return pk
}
// stackToItem converts a network ItemStack representation back to an item.Stack.
func stackToItem(it protocol.ItemStack) item.Stack {
t, ok := world.ItemByRuntimeID(it.NetworkID, int16(it.MetadataValue))
if !ok {
t = block.Air{}
}
if it.BlockRuntimeID > 0 {
// It shouldn't matter if it (for whatever reason) wasn't able to get the block runtime ID,
// since on the next line, we assert that the block is an item. If it didn't succeed, it'll
// return air anyway.
b, _ := world.BlockByRuntimeID(uint32(it.BlockRuntimeID))
if t, ok = b.(world.Item); !ok {
t = block.Air{}
}
}
//noinspection SpellCheckingInspection
if nbter, ok := t.(world.NBTer); ok && len(it.NBTData) != 0 {
t = nbter.DecodeNBT(it.NBTData).(world.Item)
}
s := item.NewStack(t, int(it.Count))
return nbtconv.ReadItem(it.NBTData, &s)
}

View File

@ -1,13 +1,14 @@
package world
import (
"bytes"
"image"
"image/draw"
"math"
"os"
"sync"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"golang.design/x/lockfree"
@ -15,47 +16,49 @@ import (
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
"golang.org/x/image/bmp"
)
const VIEW_MAP_ID = 0x424242
const ViewMapID = 0x424242
// packet to tell the client that it has a map with id 0x424242 in the offhand
var MAP_ITEM_PACKET packet.InventoryContent = packet.InventoryContent{
// MapItemPacket tells the client that it has a map with id 0x424242 in the offhand
var MapItemPacket packet.InventoryContent = packet.InventoryContent{
WindowID: 119,
Content: []protocol.ItemInstance{
{
StackNetworkID: 1,
StackNetworkID: 1, // random if auth inv
Stack: protocol.ItemStack{
ItemType: protocol.ItemType{
NetworkID: 420,
NetworkID: 420, // overwritten in onconnect
MetadataValue: 0,
},
BlockRuntimeID: 0,
Count: 1,
NBTData: map[string]interface{}{
"map_uuid": int64(VIEW_MAP_ID),
"map_name_index": int64(1),
"map_uuid": int64(ViewMapID),
},
},
},
},
}
func (m *MapUI) get_bounds() (min, max protocol.ChunkPos) {
func (m *MapUI) GetBounds() (min, max protocol.ChunkPos) {
// get the chunk coord bounds
i := 0
for _ch := range m.renderedChunks {
if _ch.X() < min.X() {
if _ch.X() < min.X() || i == 0 {
min[0] = _ch.X()
}
if _ch.Z() < min.Z() {
if _ch.Z() < min.Z() || i == 0 {
min[1] = _ch.Z()
}
if _ch.X() > max.X() {
if _ch.X() > max.X() || i == 0 {
max[0] = _ch.X()
}
if _ch.Z() > max.Z() {
if _ch.Z() > max.Z() || i == 0 {
max[1] = _ch.Z()
}
i++
}
return
}
@ -71,6 +74,8 @@ type MapUI struct {
renderQueue *lockfree.Queue
renderedChunks map[protocol.ChunkPos]*image.RGBA // prerendered chunks
needRedraw bool // when the map has updated this is true
showOnGui bool
l sync.RWMutex
ticker *time.Ticker
w *WorldState
@ -89,6 +94,27 @@ func NewMapUI(w *WorldState) *MapUI {
}
func (m *MapUI) Start() {
r := m.w.gui.Message("can_show_images", nil)
if r.Ok {
m.showOnGui = true
}
// init map
if m.w.proxy.Client != nil {
if err := m.w.proxy.Client.WritePacket(&packet.ClientBoundMapItemData{
MapID: ViewMapID,
Scale: 4,
MapsIncludedIn: []int64{ViewMapID},
Width: 0,
Height: 0,
Pixels: nil,
UpdateFlags: packet.MapUpdateFlagInitialisation,
}); err != nil {
logrus.Error(err)
return
}
}
m.ticker = time.NewTicker(33 * time.Millisecond)
go func() {
for range m.ticker.C {
@ -98,11 +124,12 @@ func (m *MapUI) Start() {
if m.w.proxy.Client != nil {
if err := m.w.proxy.Client.WritePacket(&packet.ClientBoundMapItemData{
MapID: VIEW_MAP_ID,
MapID: ViewMapID,
Scale: 4,
Width: 128,
Height: 128,
Pixels: utils.Img2rgba(m.img),
UpdateFlags: 2,
UpdateFlags: packet.MapUpdateFlagTexture,
}); err != nil {
logrus.Error(err)
return
@ -111,6 +138,21 @@ func (m *MapUI) Start() {
}
}
}()
go func() { // send map item
t := time.NewTicker(1 * time.Second)
for range t.C {
if m.w.ctx.Err() != nil {
return
}
if m.w.proxy.Client != nil {
err := m.w.proxy.Client.WritePacket(&MapItemPacket)
if err != nil {
logrus.Error(err)
return
}
}
}
}()
}
func (m *MapUI) Stop() {
@ -121,7 +163,9 @@ func (m *MapUI) Stop() {
// Reset resets the map to inital state
func (m *MapUI) Reset() {
m.l.Lock()
m.renderedChunks = make(map[protocol.ChunkPos]*image.RGBA)
m.l.Unlock()
m.SchedRedraw()
}
@ -139,23 +183,10 @@ func (m *MapUI) SchedRedraw() {
m.needRedraw = true
}
// draw_img_scaled_pos draws src onto dst at bottom_left, scaled to size
func draw_img_scaled_pos(dst *image.RGBA, src *image.RGBA, bottom_left image.Point, size_scaled int) {
sbx := src.Bounds().Dx()
ratio := int(float64(sbx) / float64(size_scaled))
for x_out := bottom_left.X; x_out < bottom_left.X+size_scaled; x_out++ {
for y_out := bottom_left.Y; y_out < bottom_left.Y+size_scaled; y_out++ {
x_in := (x_out - bottom_left.X) * ratio
y_in := (y_out - bottom_left.Y) * ratio
c := src.At(x_in, y_in)
dst.Set(x_out, y_out, c)
}
}
}
// draw chunk images to the map image
// Redraw draws chunk images to the map image
func (m *MapUI) Redraw() {
m.l.Lock()
updatedChunks := []protocol.ChunkPos{}
for {
r, ok := m.renderQueue.Dequeue().(*RenderElem)
if !ok {
@ -164,9 +195,11 @@ func (m *MapUI) Redraw() {
if r.ch != nil {
m.renderedChunks[r.pos] = Chunk2Img(r.ch)
} else {
m.renderedChunks[r.pos] = black_16x16
m.renderedChunks[r.pos] = black16x16
}
updatedChunks = append(updatedChunks, r.pos)
}
m.l.Unlock()
middle := protocol.ChunkPos{
int32(m.w.PlayerPos.Position.X()),
@ -174,60 +207,62 @@ func (m *MapUI) Redraw() {
}
// total_width := 32 * math.Ceil(float64(chunks_x)/32)
chunks_per_line := float64(128 / m.zoomLevel)
px_per_block := 128 / chunks_per_line / 16 // how many pixels per block
sz_chunk := int(math.Floor(px_per_block * 16))
chunksPerLine := float64(128 / m.zoomLevel)
pxPerBlock := 128 / chunksPerLine / 16 // how many pixels per block
pxSizeChunk := int(math.Floor(pxPerBlock * 16))
for i := 0; i < len(m.img.Pix); i++ { // clear canvas
m.img.Pix[i] = 0
}
m.l.RLock()
for _ch := range m.renderedChunks {
relative_middle_x := float64(_ch.X()*16 - middle.X())
relative_middle_z := float64(_ch.Z()*16 - middle.Z())
px_pos := image.Point{ // bottom left corner of the chunk on the map
X: int(math.Floor(relative_middle_x*px_per_block)) + 64,
Y: int(math.Floor(relative_middle_z*px_per_block)) + 64,
relativeMiddleX := float64(_ch.X()*16 - middle.X())
relativeMiddleZ := float64(_ch.Z()*16 - middle.Z())
px := image.Point{ // bottom left corner of the chunk on the map
X: int(math.Floor(relativeMiddleX*pxPerBlock)) + 64,
Y: int(math.Floor(relativeMiddleZ*pxPerBlock)) + 64,
}
if !m.img.Rect.Intersect(image.Rect(px_pos.X, px_pos.Y, px_pos.X+sz_chunk, px_pos.Y+sz_chunk)).Empty() {
draw_img_scaled_pos(m.img, m.renderedChunks[_ch], px_pos, sz_chunk)
if !m.img.Rect.Intersect(image.Rect(px.X, px.Y, px.X+pxSizeChunk, px.Y+pxSizeChunk)).Empty() {
utils.DrawImgScaledPos(m.img, m.renderedChunks[_ch], px, pxSizeChunk)
}
}
draw_full := false
if draw_full {
img2 := m.ToImage()
buf := bytes.NewBuffer(nil)
bmp.Encode(buf, img2)
os.WriteFile("test.bmp", buf.Bytes(), 0o777)
ChunkCount := len(m.renderedChunks)
if m.showOnGui {
min, max := m.GetBounds()
m.w.gui.Message(messages.UpdateMap, messages.UpdateMapPayload{
ChunkCount: ChunkCount,
Rotation: m.w.PlayerPos.Yaw,
UpdatedTiles: updatedChunks,
Tiles: m.renderedChunks,
BoundsMin: min,
BoundsMax: max,
})
}
m.l.RUnlock()
}
func (m *MapUI) ToImage() *image.RGBA {
// get the chunk coord bounds
min, max := m.get_bounds()
chunks_x := int(max[0] - min[0] + 1) // how many chunk lengths is x coordinate
chunks_y := int(max[1] - min[1] + 1)
min, max := m.GetBounds()
chunksX := int(max[0] - min[0] + 1) // how many chunk lengths is x coordinate
chunksY := int(max[1] - min[1] + 1)
img2 := image.NewRGBA(image.Rect(0, 0, chunks_x*16, chunks_y*16))
img2 := image.NewRGBA(image.Rect(0, 0, chunksX*16, chunksY*16))
middle_block_x := chunks_x / 2 * 16
middle_block_y := chunks_y / 2 * 16
for pos := range m.renderedChunks {
px_pos := image.Point{
X: int(pos.X()*16) - middle_block_x + img2.Rect.Dx(),
Y: int(pos.Z()*16) - middle_block_y + img2.Rect.Dy(),
}
m.l.RLock()
for pos, tile := range m.renderedChunks {
px := image.Pt(
int((pos.X()-min.X())*16),
int((pos.Z()-min.Z())*16),
)
draw.Draw(img2, image.Rect(
px_pos.X,
px_pos.Y,
px_pos.X+16,
px_pos.Y+16,
), m.renderedChunks[pos], image.Point{}, draw.Src)
px.X, px.Y,
px.X+16, px.Y+16,
), tile, image.Point{}, draw.Src)
}
m.l.RUnlock()
return img2
}
@ -235,3 +270,27 @@ func (m *MapUI) SetChunk(pos protocol.ChunkPos, ch *chunk.Chunk) {
m.renderQueue.Enqueue(&RenderElem{pos, ch})
m.SchedRedraw()
}
func (w *WorldState) ProcessAnimate(pk *packet.Animate) {
if pk.ActionType == packet.AnimateActionSwingArm {
w.mapUI.ChangeZoom()
w.proxy.SendPopup(locale.Loc("zoom_level", locale.Strmap{"Level": w.mapUI.zoomLevel}))
}
}
func (w *WorldState) processMapPacketsClient(pk packet.Packet, forward *bool) packet.Packet {
switch pk := pk.(type) {
case *packet.MovePlayer:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.PlayerAuthInput:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.MapInfoRequest:
if pk.MapID == ViewMapID {
w.mapUI.SchedRedraw()
*forward = false
}
case *packet.Animate:
w.ProcessAnimate(pk)
}
return pk
}

View File

@ -3,36 +3,32 @@ package world
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"image"
"image/draw"
"image/png"
"math/rand"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/bedrock-tool/bedrocktool/utils/nbtconv"
"github.com/bedrock-tool/bedrocktool/utils/behaviourpack"
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/df-mc/dragonfly/server/world/mcdb"
"github.com/df-mc/goleveldb/leveldb/opt"
"github.com/go-gl/mathgl/mgl32"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
//_ "github.com/df-mc/dragonfly/server/block" // to load blocks
//_ "net/http/pprof"
)
type TPlayerPos struct {
@ -42,54 +38,61 @@ type TPlayerPos struct {
HeadYaw float32
}
type itemContainer struct {
OpenPacket *packet.ContainerOpen
Content *packet.InventoryContent
}
// the state used for drawing and saving
type WorldState struct {
ctx context.Context
ispre118 bool
voidgen bool
ctx context.Context
proxy *utils.ProxyContext
mapUI *MapUI
gui utils.UI
bp *behaviourpack.BehaviourPack
// save state
chunks map[protocol.ChunkPos]*chunk.Chunk
blockNBT map[protocol.SubChunkPos][]map[string]any
openItemContainers map[byte]*itemContainer
airRid uint32
Dim world.Dimension
WorldName string
ServerName string
worldCounter int
packs map[string]*resource.Pack
entities map[uint64]*entityState
Dim world.Dimension
PlayerPos TPlayerPos
worldCounter int
WorldName string
ServerName string
ispre118 bool
// settings
voidGen bool
withPacks bool
saveImage bool
experimentInventory bool
PlayerPos TPlayerPos
proxy *utils.ProxyContext
// ui
ui *MapUI
}
func NewWorldState() *WorldState {
func NewWorldState(ctx context.Context, proxy *utils.ProxyContext, ServerName string, ui utils.UI) *WorldState {
w := &WorldState{
ctx: ctx,
proxy: proxy,
mapUI: nil,
gui: ui,
bp: behaviourpack.New(ServerName),
ServerName: ServerName,
chunks: make(map[protocol.ChunkPos]*chunk.Chunk),
blockNBT: make(map[protocol.SubChunkPos][]map[string]any),
openItemContainers: make(map[byte]*itemContainer),
entities: make(map[uint64]*entityState),
Dim: nil,
WorldName: "world",
PlayerPos: TPlayerPos{},
airRid: 6692,
}
w.ui = NewMapUI(w)
w.mapUI = NewMapUI(w)
w.gui.Message(messages.Init, messages.InitPayload{
Handler: w.uiMessage,
})
return w
}
var dimension_ids = map[uint8]world.Dimension{
var dimensionIDMap = map[uint8]world.Dimension{
0: world.Overworld,
1: world.Nether,
2: world.End,
@ -100,190 +103,104 @@ var dimension_ids = map[uint8]world.Dimension{
}
var (
black_16x16 = image.NewRGBA(image.Rect(0, 0, 16, 16))
Offset_table [24]protocol.SubChunkOffset
black16x16 = image.NewRGBA(image.Rect(0, 0, 16, 16))
offsetTable [24]protocol.SubChunkOffset
)
func init() {
for i := range Offset_table {
Offset_table[i] = protocol.SubChunkOffset{0, int8(i), 0}
for i := range offsetTable {
offsetTable[i] = protocol.SubChunkOffset{0, int8(i), 0}
}
for i := 3; i < len(black16x16.Pix); i += 4 {
black16x16.Pix[i] = 255
}
draw.Draw(black_16x16, image.Rect(0, 0, 16, 16), image.Black, image.Point{}, draw.Src)
utils.RegisterCommand(&WorldCMD{})
}
type WorldCMD struct {
Address string
packs bool
enableVoid bool
saveImage bool
experimentInventory bool
ServerAddress string
Packs bool
EnableVoid bool
SaveImage bool
ExperimentInventory bool
}
func (*WorldCMD) Name() string { return "worlds" }
func (*WorldCMD) Synopsis() string { return "download a world from a server" }
func (*WorldCMD) Synopsis() string { return locale.Loc("world_synopsis", nil) }
func (p *WorldCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&p.Address, "address", "", "remote server address")
f.BoolVar(&p.packs, "packs", false, "save resourcepacks to the worlds")
f.BoolVar(&p.enableVoid, "void", true, "if false, saves with default flat generator")
f.BoolVar(&p.saveImage, "image", false, "saves an png of the map at the end")
f.BoolVar(&p.experimentInventory, "inv", false, "enable experimental block inventory saving")
func (c *WorldCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.ServerAddress, "address", "", locale.Loc("remote_address", nil))
f.BoolVar(&c.Packs, "packs", false, locale.Loc("save_packs_with_world", nil))
f.BoolVar(&c.EnableVoid, "void", true, locale.Loc("enable_void", nil))
f.BoolVar(&c.SaveImage, "image", false, locale.Loc("save_image", nil))
f.BoolVar(&c.ExperimentInventory, "inv", false, locale.Loc("test_block_inv", nil))
}
func (c *WorldCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n" + utils.SERVER_ADDRESS_HELP
}
func (c *WorldCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
/*
go func() {
http.ListenAndServe(":8000", nil)
}()
*/
server_address, hostname, err := utils.ServerInput(ctx, c.Address)
func (c *WorldCMD) Execute(ctx context.Context, ui utils.UI) error {
serverAddress, hostname, err := ui.ServerInput(ctx, c.ServerAddress)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
return err
}
w := NewWorldState()
w.voidgen = c.enableVoid
w.ServerName = hostname
w.withPacks = c.packs
w.saveImage = c.saveImage
w.experimentInventory = c.experimentInventory
w.ctx = ctx
proxy, err := utils.NewProxy()
if err != nil {
return err
}
proxy := utils.NewProxy(logrus.StandardLogger())
w := NewWorldState(ctx, proxy, hostname, ui)
w.voidGen = c.EnableVoid
w.withPacks = c.Packs
w.saveImage = c.SaveImage
w.experimentInventory = c.ExperimentInventory
proxy.AlwaysGetPacks = true
proxy.ConnectCB = w.OnConnect
proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool) (packet.Packet, error) {
var forward bool
proxy.OnClientConnect = func(proxy *utils.ProxyContext, hasClient bool) {
w.gui.Message(messages.SetUIState, messages.UIStateConnecting)
}
proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool, _ time.Time) (packet.Packet, error) {
forward := true
if toServer {
pk, forward = w.ProcessPacketClient(pk)
// from client
pk = w.processItemPacketsClient(pk, &forward)
pk = w.processMapPacketsClient(pk, &forward)
} else {
pk, forward = w.ProcessPacketServer(pk)
// from server
pk = w.processItemPacketsServer(pk)
pk = w.ProcessChunkPackets(pk)
pk = w.ProcessEntityPackets(pk)
}
if !forward {
return nil, nil
}
return pk, nil
}
err = proxy.Run(ctx, server_address)
w.gui.Message(messages.SetUIState, messages.UIStateConnect)
err = w.proxy.Run(ctx, serverAddress)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return err
}
w.SaveAndReset()
return 0
return nil
}
func (w *WorldState) setnameCommand(cmdline []string) bool {
w.WorldName = strings.Join(cmdline, " ")
w.proxy.SendMessage(fmt.Sprintf("worldName is now: %s", w.WorldName))
return true
}
func (w *WorldState) toggleVoid(cmdline []string) bool {
w.voidgen = !w.voidgen
w.proxy.SendMessage(fmt.Sprintf("using void generator: %t", w.voidgen))
return true
}
func (w *WorldState) ProcessLevelChunk(pk *packet.LevelChunk) {
_, exists := w.chunks[pk.Position]
if exists {
return
func (w *WorldState) uiMessage(name string, data interface{}) messages.MessageResponse {
r := messages.MessageResponse{
Ok: false,
Data: nil,
}
ch, blockNBTs, err := chunk.NetworkDecode(w.airRid, pk.RawPayload, int(pk.SubChunkCount), w.Dim.Range(), w.ispre118)
if err != nil {
logrus.Error(err)
return
switch name {
case messages.SetVoidGen:
set_void_gen := data.(messages.SetVoidGenPayload)
r.Ok = w.setVoidGen(set_void_gen.Value, true)
case messages.SetWorldName:
set_world_name := data.(messages.SetWorldNamePayload)
r.Ok = w.setWorldName(set_world_name.WorldName, true)
}
if blockNBTs != nil {
w.blockNBT[protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
}] = blockNBTs
}
w.chunks[pk.Position] = ch
if pk.SubChunkRequestMode == protocol.SubChunkRequestModeLegacy {
w.ui.SetChunk(pk.Position, ch)
} else {
w.ui.SetChunk(pk.Position, nil)
// request all the subchunks
max := w.Dim.Range().Height() / 16
if pk.SubChunkRequestMode == protocol.SubChunkRequestModeLimited {
max = int(pk.HighestSubChunk)
}
w.proxy.Server.WritePacket(&packet.SubChunkRequest{
Dimension: int32(w.Dim.EncodeDimension()),
Position: protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
},
Offsets: Offset_table[:max],
})
}
}
func (w *WorldState) ProcessSubChunk(pk *packet.SubChunk) {
pos_to_redraw := make(map[protocol.ChunkPos]bool)
for _, sub := range pk.SubChunkEntries {
var (
abs_x = pk.Position[0] + int32(sub.Offset[0])
abs_y = pk.Position[1] + int32(sub.Offset[1])
abs_z = pk.Position[2] + int32(sub.Offset[2])
subpos = protocol.SubChunkPos{abs_x, abs_y, abs_z}
pos = protocol.ChunkPos{abs_x, abs_z}
)
ch := w.chunks[pos]
if ch == nil {
logrus.Errorf("the server didnt send the chunk before the subchunk!")
continue
}
blockNBT, err := ch.ApplySubChunkEntry(uint8(abs_y), &sub)
if err != nil {
logrus.Error(err)
}
if blockNBT != nil {
w.blockNBT[subpos] = blockNBT
}
pos_to_redraw[pos] = true
}
// redraw the chunks
for pos := range pos_to_redraw {
w.ui.SetChunk(pos, w.chunks[pos])
}
w.ui.SchedRedraw()
}
func (w *WorldState) ProcessAnimate(pk *packet.Animate) {
if pk.ActionType == packet.AnimateActionSwingArm {
w.ui.ChangeZoom()
w.proxy.SendPopup(fmt.Sprintf("Zoom: %d", w.ui.zoomLevel))
}
}
func (w *WorldState) ProcessChangeDimension(pk *packet.ChangeDimension) {
if len(w.chunks) > 0 {
w.SaveAndReset()
} else {
logrus.Info("Skipping save because the world didnt contain any chunks.")
w.Reset()
}
dim_id := pk.Dimension
if w.ispre118 {
dim_id += 10
}
w.Dim = dimension_ids[uint8(dim_id)]
return r
}
func (w *WorldState) SetPlayerPos(Position mgl32.Vec3, Pitch, Yaw, HeadYaw float32) {
@ -296,19 +213,74 @@ func (w *WorldState) SetPlayerPos(Position mgl32.Vec3, Pitch, Yaw, HeadYaw float
}
if int(last.Position.X()) != int(w.PlayerPos.Position.X()) || int(last.Position.Z()) != int(w.PlayerPos.Position.Z()) {
w.ui.SchedRedraw()
w.mapUI.SchedRedraw()
}
}
func (w *WorldState) setVoidGen(val bool, fromUI bool) bool {
w.voidGen = val
var s string
if w.voidGen {
s = locale.Loc("void_generator_true", nil)
} else {
s = locale.Loc("void_generator_false", nil)
}
w.proxy.SendMessage(s)
if !fromUI {
w.gui.Message(messages.SetVoidGen, messages.SetVoidGenPayload{
Value: w.voidGen,
})
}
return true
}
func (w *WorldState) setWorldName(val string, fromUI bool) bool {
w.WorldName = val
w.proxy.SendMessage(locale.Loc("worldname_set", locale.Strmap{"Name": w.WorldName}))
if !fromUI {
w.gui.Message(messages.SetWorldName, messages.SetWorldNamePayload{
WorldName: w.WorldName,
})
}
return true
}
func (w *WorldState) Reset() {
w.chunks = make(map[protocol.ChunkPos]*chunk.Chunk)
w.WorldName = fmt.Sprintf("world-%d", w.worldCounter)
w.ui.Reset()
w.mapUI.Reset()
}
// writes the world to a folder, resets all the chunks
// SaveAndReset writes the world to a folder, resets all the chunks
func (w *WorldState) SaveAndReset() {
logrus.Infof("Saving world %s %d chunks", w.WorldName, len(w.chunks))
keys := make([]protocol.ChunkPos, 0, len(w.chunks))
for cp := range w.chunks {
keys = append(keys, cp)
}
for _, cp := range keys {
has_any := false
for _, sc := range w.chunks[cp].Sub() {
has_any = !sc.Empty()
if has_any {
break
}
}
if !has_any {
delete(w.chunks, cp)
}
}
if len(w.chunks) == 0 {
w.Reset()
return
}
logrus.Infof(locale.Loc("saving_world", locale.Strmap{"Name": w.WorldName, "Count": len(w.chunks)}))
// open world
folder := path.Join("worlds", fmt.Sprintf("%s/%s", w.ServerName, w.WorldName))
@ -326,13 +298,26 @@ func (w *WorldState) SaveAndReset() {
}
// save block nbt data
blockNBT := make(map[protocol.ChunkPos][]map[string]any)
blockNBT := make(map[world.ChunkPos][]map[string]any)
for scp, v := range w.blockNBT { // 3d to 2d
cp := protocol.ChunkPos{scp.X(), scp.Z()}
cp := world.ChunkPos{scp.X(), scp.Z()}
blockNBT[cp] = append(blockNBT[cp], v...)
}
for cp, v := range blockNBT {
err = provider.SaveBlockNBT((world.ChunkPos)(cp), v, w.Dim)
err = provider.SaveBlockNBT(cp, v, w.Dim)
if err != nil {
logrus.Error(err)
}
}
chunkEntities := make(map[world.ChunkPos][]world.Entity)
for _, es := range w.entities {
cp := world.ChunkPos{int32(es.Position.X()) >> 4, int32(es.Position.Z()) >> 4}
chunkEntities[cp] = append(chunkEntities[cp], es.ToServerEntity())
}
for cp, v := range chunkEntities {
err = provider.SaveEntities(cp, v, w.Dim)
if err != nil {
logrus.Error(err)
}
@ -415,66 +400,143 @@ func (w *WorldState) SaveAndReset() {
ld.ShowBorderEffect = gr.Value.(bool)
// todo
default:
logrus.Warnf("unknown gamerule: %s\n", gr.Name)
logrus.Warnf(locale.Loc("unknown_gamerule", locale.Strmap{"Name": gr.Name}))
}
}
ld.RandomSeed = int64(gd.WorldSeed)
// void world
if w.voidgen {
if w.voidGen {
ld.FlatWorldLayers = `{"biome_id":1,"block_layers":[{"block_data":0,"block_id":0,"count":1},{"block_data":0,"block_id":0,"count":2},{"block_data":0,"block_id":0,"count":1}],"encoding_version":3,"structure_options":null}`
ld.Generator = 2
}
if w.bp.HasContent() {
if ld.Experiments == nil {
ld.Experiments = map[string]any{}
}
ld.Experiments["data_driven_items"] = true
ld.Experiments["experiments_ever_used"] = true
ld.Experiments["saved_with_toggled_experiments"] = true
}
provider.SaveSettings(s)
provider.Close()
if err = provider.Close(); err != nil {
logrus.Error(err)
}
w.worldCounter += 1
for k, p := range w.packs {
logrus.Infof("Adding resource pack: %s\n", k)
pack_folder := path.Join(folder, "resource_packs", k)
os.MkdirAll(pack_folder, 0o755)
data := make([]byte, p.Len())
p.ReadAt(data, 0)
utils.UnpackZip(bytes.NewReader(data), int64(len(data)), pack_folder)
type dep struct {
PackID string `json:"pack_id"`
Version [3]int `json:"version"`
}
addPacksJSON := func(name string, deps []dep) {
f, err := os.Create(path.Join(folder, name))
if err != nil {
logrus.Error(err)
return
}
defer f.Close()
if err := json.NewEncoder(f).Encode(deps); err != nil {
logrus.Error(err)
return
}
}
// save behaviourpack
if w.bp.HasContent() {
name := strings.ReplaceAll(w.ServerName, "./", "")
name = strings.ReplaceAll(name, "/", "-")
packFolder := path.Join(folder, "behavior_packs", name)
os.MkdirAll(packFolder, 0o755)
for _, p := range w.proxy.Server.ResourcePacks() {
p := utils.PackFromBase(p)
w.bp.CheckAddLink(p)
}
w.bp.Save(packFolder)
addPacksJSON("world_behavior_packs.json", []dep{{
PackID: w.bp.Manifest.Header.UUID,
Version: w.bp.Manifest.Header.Version,
}})
// force resource packs for worlds with custom blocks
w.withPacks = true
}
// add resource packs
if w.withPacks {
packs, err := utils.GetPacks(w.proxy.Server)
if err != nil {
logrus.Error(err)
} else {
var rdeps []dep
for k, p := range packs {
logrus.Infof(locale.Loc("adding_pack", locale.Strmap{"Name": k}))
packFolder := path.Join(folder, "resource_packs", p.Name())
os.MkdirAll(packFolder, 0o755)
data := make([]byte, p.Len())
p.ReadAt(data, 0)
utils.UnpackZip(bytes.NewReader(data), int64(len(data)), packFolder)
rdeps = append(rdeps, dep{
PackID: p.Manifest().Header.UUID,
Version: p.Manifest().Header.Version,
})
}
if len(rdeps) > 0 {
addPacksJSON("world_resource_packs.json", rdeps)
}
}
}
if w.saveImage {
f, _ := os.Create(folder + ".png")
png.Encode(f, w.ui.ToImage())
png.Encode(f, w.mapUI.ToImage())
f.Close()
}
// zip it
filename := folder + ".mcworld"
if err := utils.ZipFolder(filename, folder); err != nil {
fmt.Println(err)
}
logrus.Infof("Saved: %s\n", filename)
os.RemoveAll(folder)
logrus.Info(locale.Loc("saved", locale.Strmap{"Name": filename}))
//os.RemoveAll(folder)
w.Reset()
}
func (w *WorldState) OnConnect(proxy *utils.ProxyContext) {
func (w *WorldState) OnConnect(proxy *utils.ProxyContext, err error) bool {
w.gui.Message(messages.SetUIState, messages.UIStateMain)
if err != nil {
return false
}
w.proxy = proxy
gd := w.proxy.Server.GameData()
if len(gd.CustomBlocks) > 0 {
logrus.Info("Using Custom Blocks")
/*
for _, be := range gd.CustomBlocks {
b := block.ServerCustomBlock(be)
world.RegisterBlock(b)
}
*/
world.InsertCustomItems(gd.Items)
for _, ie := range gd.Items {
w.bp.AddItem(ie)
}
if w.withPacks {
go func() {
w.packs, _ = utils.GetPacks(w.proxy.Server)
}()
mapItemID, _ := world.ItemRidByName("minecraft:filled_map")
MapItemPacket.Content[0].Stack.ItemType.NetworkID = mapItemID
if gd.ServerAuthoritativeInventory {
MapItemPacket.Content[0].StackNetworkID = 0xffff + rand.Int31n(0xfff)
}
if len(gd.CustomBlocks) > 0 {
logrus.Info(locale.Loc("using_customblocks", nil))
for _, be := range gd.CustomBlocks {
w.bp.AddBlock(be)
}
// telling the chunk code what custom blocks there are so it can generate offsets
world.InsertCustomBlocks(gd.CustomBlocks)
}
{ // check game version
@ -486,42 +548,28 @@ func (w *WorldState) OnConnect(proxy *utils.ProxyContext) {
w.ispre118 = ver < 18
}
if err != nil || len(gv) <= 1 {
logrus.Info("couldnt determine game version, assuming > 1.18")
logrus.Info(locale.Loc("guessing_version", nil))
}
dim_id := gd.Dimension
dimensionID := gd.Dimension
if w.ispre118 {
logrus.Info("using legacy (< 1.18)")
dim_id += 10
logrus.Info(locale.Loc("using_under_118", nil))
dimensionID += 10
}
w.Dim = dimension_ids[uint8(dim_id)]
w.Dim = dimensionIDMap[uint8(dimensionID)]
}
w.proxy.SendMessage("use /setname <worldname>\nto set the world name")
w.proxy.SendMessage(locale.Loc("use_setname", nil))
w.ui.Start()
go func() { // send map item
select {
case <-w.ctx.Done():
return
default:
for {
time.Sleep(1 * time.Second)
if w.proxy.Client != nil {
err := w.proxy.Client.WritePacket(&MAP_ITEM_PACKET)
if err != nil {
return
}
}
}
}
}()
w.mapUI.Start()
proxy.AddCommand(utils.IngameCommand{
Exec: w.setnameCommand,
Exec: func(cmdline []string) bool {
return w.setWorldName(strings.Join(cmdline, " "), false)
},
Cmd: protocol.Command{
Name: "setname",
Description: "set user defined name for this world",
Description: locale.Loc("setname_desc", nil),
Overloads: []protocol.CommandOverload{
{
Parameters: []protocol.CommandParameter{
@ -537,139 +585,14 @@ func (w *WorldState) OnConnect(proxy *utils.ProxyContext) {
})
proxy.AddCommand(utils.IngameCommand{
Exec: w.toggleVoid,
Exec: func(cmdline []string) bool {
return w.setVoidGen(!w.voidGen, false)
},
Cmd: protocol.Command{
Name: "void",
Description: "toggle if void generator should be used",
Description: locale.Loc("void_desc", nil),
},
})
}
func (w *WorldState) ProcessPacketClient(pk packet.Packet) (packet.Packet, bool) {
forward := true
switch pk := pk.(type) {
case *packet.MovePlayer:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.PlayerAuthInput:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.MapInfoRequest:
if pk.MapID == VIEW_MAP_ID {
w.ui.SchedRedraw()
forward = false
}
case *packet.MobEquipment:
if pk.NewItem.Stack.NBTData["map_uuid"] == int64(VIEW_MAP_ID) {
forward = false
}
case *packet.Animate:
w.ProcessAnimate(pk)
}
return pk, forward
}
// stackToItem converts a network ItemStack representation back to an item.Stack.
func stackToItem(it protocol.ItemStack) item.Stack {
t, ok := world.ItemByRuntimeID(it.NetworkID, int16(it.MetadataValue))
if !ok {
t = block.Air{}
}
if it.BlockRuntimeID > 0 {
// It shouldn't matter if it (for whatever reason) wasn't able to get the block runtime ID,
// since on the next line, we assert that the block is an item. If it didn't succeed, it'll
// return air anyway.
b, _ := world.BlockByRuntimeID(uint32(it.BlockRuntimeID))
if t, ok = b.(world.Item); !ok {
t = block.Air{}
}
}
//noinspection SpellCheckingInspection
if nbter, ok := t.(world.NBTer); ok && len(it.NBTData) != 0 {
t = nbter.DecodeNBT(it.NBTData).(world.Item)
}
s := item.NewStack(t, int(it.Count))
return nbtconv.ReadItem(it.NBTData, &s)
}
func (w *WorldState) ProcessPacketServer(pk packet.Packet) (packet.Packet, bool) {
switch pk := pk.(type) {
case *packet.ChangeDimension:
w.ProcessChangeDimension(pk)
case *packet.LevelChunk:
w.ProcessLevelChunk(pk)
w.proxy.SendPopup(fmt.Sprintf("%d chunks loaded\nname: %s", len(w.chunks), w.WorldName))
case *packet.SubChunk:
w.ProcessSubChunk(pk)
case *packet.ContainerOpen:
if w.experimentInventory {
// add to open containers
existing, ok := w.openItemContainers[pk.WindowID]
if !ok {
existing = &itemContainer{}
}
w.openItemContainers[pk.WindowID] = &itemContainer{
OpenPacket: pk,
Content: existing.Content,
}
}
case *packet.InventoryContent:
if w.experimentInventory {
// save content
fmt.Printf("WindowID: %d\n", pk.WindowID)
existing, ok := w.openItemContainers[byte(pk.WindowID)]
if !ok {
if pk.WindowID == 0x0 { // inventory
w.openItemContainers[byte(pk.WindowID)] = &itemContainer{
Content: pk,
}
}
break
}
existing.Content = pk
}
case *packet.ContainerClose:
if w.experimentInventory {
switch pk.WindowID {
case protocol.WindowIDArmour: // todo handle
case protocol.WindowIDOffHand: // todo handle
case protocol.WindowIDUI:
case protocol.WindowIDInventory: // todo handle
default:
// find container info
existing, ok := w.openItemContainers[byte(pk.WindowID)]
if !ok {
logrus.Warn("Closed window that wasnt open")
break
}
if existing.Content == nil {
break
}
pos := existing.OpenPacket.ContainerPosition
cp := protocol.SubChunkPos{pos.X() << 4, pos.Z() << 4}
// create inventory
inv := inventory.New(len(existing.Content.Content), nil)
for i, c := range existing.Content.Content {
item := stackToItem(c.Stack)
inv.SetItem(i, item)
}
// put into subchunk
nbts := w.blockNBT[cp]
for i, v := range nbts {
nbt_pos := protocol.BlockPos{v["x"].(int32), v["y"].(int32), v["z"].(int32)}
if nbt_pos == pos {
w.blockNBT[cp][i]["Items"] = nbtconv.InvToNBT(inv)
}
}
w.proxy.SendMessage("Saved Block Inventory")
// remove it again
delete(w.openItemContainers, byte(pk.WindowID))
}
}
}
return pk, true
return true
}

140
ui/gui.go Normal file
View File

@ -0,0 +1,140 @@
//go:build gui || android
package ui
import (
"context"
"image/color"
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget/material"
"gioui.org/x/pref/theme"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages/settings"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages/skins"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages/worlds"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sirupsen/logrus"
)
type GUI struct {
utils.BaseUI
router pages.Router
cancel context.CancelFunc
}
func (g *GUI) Init() bool {
utils.SetCurrentUI(g)
return true
}
var paletteLight = material.Palette{
Bg: color.NRGBA{0xff, 0xff, 0xff, 0xff},
Fg: color.NRGBA{0x12, 0x12, 0x12, 0xff},
ContrastBg: color.NRGBA{0x7c, 0x00, 0xf8, 0xff},
ContrastFg: color.NRGBA{0x00, 0x00, 0x00, 0xff},
}
var paletteDark = material.Palette{
Bg: color.NRGBA{0x12, 0x12, 0x12, 0xff},
Fg: color.NRGBA{0xff, 0xff, 0xff, 0xff},
ContrastBg: color.NRGBA{0x7c, 0x00, 0xf8, 0xff},
ContrastFg: color.NRGBA{0xff, 0xff, 0xff, 0xff},
}
func (g *GUI) Start(ctx context.Context, cancel context.CancelFunc) (err error) {
g.cancel = cancel
w := app.NewWindow(
app.Title("Bedrocktool"),
)
th := material.NewTheme(gofont.Collection())
dark, err := theme.IsDarkMode()
if err != nil {
logrus.Warn(err)
}
if dark {
_th := th.WithPalette(paletteDark)
th = &_th
} else {
_th := th.WithPalette(paletteLight)
th = &_th
}
g.router = pages.NewRouter(ctx, w.Invalidate, th)
g.router.Register("Settings", settings.New(&g.router))
g.router.Register("worlds", worlds.New(&g.router))
g.router.Register("skins", skins.New(&g.router))
g.router.SwitchTo("Settings")
go func() {
err = g.run(w)
}()
go func() {
app.Main()
}()
<-ctx.Done()
return err
}
func (g *GUI) run(w *app.Window) error {
var ops op.Ops
for {
select {
case e := <-w.Events():
switch e := e.(type) {
case system.DestroyEvent:
logrus.Info("Closing")
g.cancel()
g.router.Wg.Wait()
return e.Err
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
g.router.Layout(gtx, g.router.Theme)
e.Frame(gtx.Ops)
}
case <-g.router.Ctx.Done():
logrus.Info("Closing")
g.cancel()
g.router.Wg.Wait()
return nil
}
}
}
func (g *GUI) Message(name string, data interface{}) messages.MessageResponse {
r := g.router.Handler(name, data)
if r.Ok || r.Data != nil {
return r
}
r = messages.MessageResponse{
Ok: false,
Data: nil,
}
switch name {
case "can_show_images":
r.Ok = true
}
return r
}
func init() {
utils.MakeGui = func() utils.UI {
return &GUI{}
}
}

148
ui/gui/pages/page.go Normal file
View File

@ -0,0 +1,148 @@
package pages
import (
"context"
"log"
"sync"
"time"
"gioui.org/layout"
"gioui.org/op/paint"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/bedrock-tool/bedrocktool/ui/messages"
)
type HandlerFunc = func(name string, data interface{}) messages.MessageResponse
type Page interface {
Actions() []component.AppBarAction
Overflow() []component.OverflowAction
Layout(gtx layout.Context, th *material.Theme) layout.Dimensions
NavItem() component.NavItem
// handle events from program
Handler() HandlerFunc
}
type Router struct {
Ctx context.Context
Wg sync.WaitGroup
Invalidate func()
Theme *material.Theme
pages map[string]Page
current string
*component.ModalNavDrawer
NavAnim component.VisibilityAnimation
*component.AppBar
*component.ModalLayer
NonModalDrawer, BottomBar bool
}
func NewRouter(ctx context.Context, invalidate func(), th *material.Theme) Router {
modal := component.NewModal()
nav := component.NewNav("Navigation Drawer", "This is an example.")
modalNav := component.ModalNavFrom(&nav, modal)
bar := component.NewAppBar(modal)
//bar.NavigationIcon = icon.MenuIcon
na := component.VisibilityAnimation{
State: component.Invisible,
Duration: time.Millisecond * 250,
}
return Router{
Ctx: ctx,
Invalidate: invalidate,
Theme: th,
pages: make(map[string]Page),
ModalLayer: modal,
ModalNavDrawer: modalNav,
AppBar: bar,
NavAnim: na,
}
}
func (r *Router) Register(tag string, p Page) {
r.pages[tag] = p
navItem := p.NavItem()
navItem.Tag = tag
if r.current == "" {
r.current = tag
r.AppBar.Title = navItem.Name
r.AppBar.SetActions(p.Actions(), p.Overflow())
}
r.ModalNavDrawer.AddNavItem(navItem)
}
func (r *Router) SwitchTo(tag string) {
p, ok := r.pages[tag]
if !ok {
return
}
navItem := p.NavItem()
r.current = tag
r.AppBar.Title = navItem.Name
r.AppBar.SetActions(p.Actions(), p.Overflow())
}
func (r *Router) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
for _, event := range r.AppBar.Events(gtx) {
switch event := event.(type) {
case component.AppBarNavigationClicked:
if r.NonModalDrawer {
r.NavAnim.ToggleVisibility(gtx.Now)
} else {
r.ModalNavDrawer.Appear(gtx.Now)
r.NavAnim.Disappear(gtx.Now)
}
case component.AppBarContextMenuDismissed:
log.Printf("Context menu dismissed: %v", event)
case component.AppBarOverflowActionClicked:
log.Printf("Overflow action selected: %v", event)
}
}
if r.ModalNavDrawer.NavDestinationChanged() {
r.SwitchTo(r.ModalNavDrawer.CurrentNavDestination().(string))
}
paint.Fill(gtx.Ops, th.Palette.Bg)
content := layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Max.X /= 3
return r.NavDrawer.Layout(gtx, th, &r.NavAnim)
}),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return r.pages[r.current].Layout(gtx, th)
}),
)
})
bar := layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return r.AppBar.Layout(gtx, th, "Menu", "Actions")
})
flex := layout.Flex{Axis: layout.Vertical}
if r.BottomBar {
flex.Layout(gtx, content, bar)
} else {
flex.Layout(gtx, bar, content)
}
r.ModalLayer.Layout(gtx, th)
return layout.Dimensions{Size: gtx.Constraints.Max}
}
func (r *Router) Handler(name string, data interface{}) messages.MessageResponse {
page, ok := r.pages[r.current]
if ok {
return page.Handler()(name, data)
}
return messages.MessageResponse{}
}
var Pages = map[string]func(*Router) Page{}
func Register(name string, fun func(*Router) Page) {
Pages[name] = fun
}

View File

@ -0,0 +1,179 @@
package settings
import (
"image"
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages"
"github.com/bedrock-tool/bedrocktool/ui/gui/settings"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sirupsen/logrus"
)
type (
C = layout.Context
D = layout.Dimensions
)
type Page struct {
*pages.Router
cmdMenu struct {
show bool
open widget.Clickable
state *component.MenuState
items map[string]*widget.Clickable
selected string
}
startButton widget.Clickable
}
func New(router *pages.Router) *Page {
p := &Page{
Router: router,
startButton: widget.Clickable{},
}
options := make([]func(layout.Context) layout.Dimensions, 0, len(utils.ValidCMDs))
p.cmdMenu.items = make(map[string]*widget.Clickable, len(utils.ValidCMDs))
for k := range utils.ValidCMDs {
item := &widget.Clickable{}
p.cmdMenu.items[k] = item
options = append(options, component.MenuItem(router.Theme, item, k).Layout)
}
p.cmdMenu.state = &component.MenuState{
OptionList: layout.List{},
Options: options,
}
//p.cmdMenu.selected = "worlds"
for _, su := range settings.Settings {
su.Init()
}
return p
}
var _ pages.Page = &Page{}
func (p *Page) Actions() []component.AppBarAction {
return []component.AppBarAction{}
}
func (p *Page) Overflow() []component.OverflowAction {
return []component.OverflowAction{}
}
func (p *Page) NavItem() component.NavItem {
return component.NavItem{
Name: "Settings",
//Icon: icon.OtherIcon,
}
}
func (p *Page) Layout(gtx C, th *material.Theme) D {
if p.startButton.Clicked() {
if p.cmdMenu.selected != "" {
cmd, ok := utils.ValidCMDs[p.cmdMenu.selected]
if !ok {
logrus.Errorf("Cmd %s not found", p.cmdMenu.selected)
}
if s, ok := settings.Settings[p.cmdMenu.selected]; ok {
s.Apply()
}
p.Router.SwitchTo(p.cmdMenu.selected)
p.Router.Wg.Add(1)
go func() {
defer p.Router.Wg.Done()
utils.InitDNS()
utils.InitExtraDebug(p.Router.Ctx)
err := cmd.Execute(p.Router.Ctx, utils.CurrentUI)
if err != nil {
logrus.Error(err)
}
}()
}
}
if p.cmdMenu.open.Clicked() {
p.cmdMenu.show = !p.cmdMenu.show
}
for k, c := range p.cmdMenu.items {
if c.Clicked() {
p.cmdMenu.selected = k
p.cmdMenu.show = false
}
}
return layout.UniformInset(7).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
d := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
// Select Command Button
layout.Rigid(func(gtx C) D {
str := p.cmdMenu.selected
if str == "" {
str = "Select Command"
}
btn := material.Button(th, &p.cmdMenu.open, str)
return btn.Layout(gtx)
}),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if p.cmdMenu.selected == "" {
return layout.Dimensions{}
}
s, ok := settings.Settings[p.cmdMenu.selected]
if !ok {
return layout.Center.Layout(gtx, material.H4(th, "No Settings Yet (Use CLI)").Layout)
} else {
return layout.UniformInset(15).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return s.Layout(gtx, th)
})
}
}),
// Start Button
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: unit.Dp(15),
Bottom: unit.Dp(15),
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
gtx.Constraints = layout.Constraints{
Min: image.Pt(300, 50),
Max: image.Pt(400, 50),
}
btn := material.Button(th, &p.startButton, "Start")
return btn.Layout(gtx)
})
})
}),
)
if p.cmdMenu.show {
component.Menu(th, p.cmdMenu.state).Layout(gtx)
}
return d
})
}
func (p *Page) Handler() pages.HandlerFunc {
return func(name string, data interface{}) messages.MessageResponse {
return messages.MessageResponse{
Ok: false,
Data: nil,
}
}
}

100
ui/gui/pages/skins/skins.go Normal file
View File

@ -0,0 +1,100 @@
package skins
import (
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/bedrock-tool/bedrocktool/ui/gui"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages"
"github.com/bedrock-tool/bedrocktool/ui/messages"
)
type (
C = layout.Context
D = layout.Dimensions
)
type Page struct {
*pages.Router
State messages.UIState
}
func New(router *pages.Router) *Page {
return &Page{
Router: router,
}
}
var _ pages.Page = &Page{}
func (p *Page) Actions() []component.AppBarAction {
return []component.AppBarAction{}
}
func (p *Page) Overflow() []component.OverflowAction {
return []component.OverflowAction{}
}
func (p *Page) NavItem() component.NavItem {
return component.NavItem{
Name: "Skin Grabber",
//Icon: icon.OtherIcon,
}
}
func (p *Page) Layout(gtx C, th *material.Theme) D {
margin := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
switch p.State {
case messages.UIStateConnect:
// display login page
return margin.Layout(gtx, material.Label(th, 100, "connect Client").Layout)
case messages.UIStateConnecting:
// display connecting to server
return margin.Layout(gtx, material.Label(th, 100, "Connecting").Layout)
case messages.UIStateMain:
// show the main ui
return margin.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Label(th, 20, "Skin Basic UI").Layout),
)
})
}
return layout.Flex{}.Layout(gtx)
}
func (u *Page) handler(name string, data interface{}) messages.MessageResponse {
r := messages.MessageResponse{
Ok: false,
Data: nil,
}
switch name {
case messages.SetUIState:
state := data.(messages.UIState)
u.State = state
u.Router.Invalidate()
r.Ok = true
case messages.Init:
init := data.(messages.InitPayload)
_ = init
r.Ok = true
}
return r
}
func (p *Page) Handler() gui.HandlerFunc {
return p.handler
}

119
ui/gui/pages/worlds/map.go Normal file
View File

@ -0,0 +1,119 @@
package worlds
import (
"image"
"image/draw"
"time"
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type Map struct {
click f32.Point
mapPos f32.Point
pos f32.Point
imageOp paint.ImageOp
zoom float32
drag gesture.Drag
scroll gesture.Scroll
MapImage *image.RGBA
BoundsMin protocol.ChunkPos
BoundsMax protocol.ChunkPos
Rotation float32
}
func (m *Map) Layout(gtx layout.Context) layout.Dimensions {
// here we loop through all the events associated with this button.
for _, e := range m.drag.Events(gtx.Metric, gtx.Queue, gesture.Both) {
switch e.Type {
case pointer.Press:
m.click = e.Position
case pointer.Drag:
m.pos = m.mapPos.Sub(m.click).Add(e.Position)
case pointer.Release:
m.mapPos = m.pos
}
}
scrollDist := m.scroll.Scroll(gtx.Metric, gtx.Queue, time.Now(), gesture.Vertical)
m.zoom -= float32(scrollDist) / 20
if m.zoom < 0.2 {
m.zoom = 0.2
}
size := gtx.Constraints.Max
if m.MapImage != nil {
m.imageOp.Add(gtx.Ops)
b := m.MapImage.Bounds()
sx := float32(b.Dx() / 2)
sy := float32(b.Dy() / 2)
op.Affine(
f32.Affine2D{}.
Scale(f32.Pt(sx, sy), f32.Pt(m.zoom, m.zoom)).
Offset(m.pos),
).Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
}
m.drag.Add(gtx.Ops)
m.scroll.Add(gtx.Ops, image.Rect(-size.X, -size.Y, size.X, size.Y))
return layout.Dimensions{
Size: size,
}
}
func drawTile(img *image.RGBA, min, pos protocol.ChunkPos, tile *image.RGBA) {
px := image.Pt(
int((pos.X()-min[0])*16),
int((pos.Z()-min[0])*16),
)
draw.Draw(img, image.Rect(
px.X, px.Y,
px.X+16, px.Y+16,
), tile, image.Point{}, draw.Src)
}
func (m *Map) Update(u *messages.UpdateMapPayload) {
if m.MapImage == nil {
m.zoom = 1
}
needNewImage := false
if m.BoundsMin != u.BoundsMin {
needNewImage = true
m.BoundsMin = u.BoundsMin
}
if m.BoundsMax != u.BoundsMax {
needNewImage = true
m.BoundsMax = u.BoundsMax
}
if needNewImage {
chunksX := int(m.BoundsMax[0] - m.BoundsMin[0] + 1) // how many chunk lengths is x coordinate
chunksY := int(m.BoundsMax[1] - m.BoundsMin[1] + 1)
m.MapImage = image.NewRGBA(image.Rect(0, 0, chunksX*16, chunksY*16))
for pos, tile := range u.Tiles {
drawTile(m.MapImage, m.BoundsMin, pos, tile)
}
} else {
for _, pos := range u.UpdatedTiles {
tile := u.Tiles[pos]
drawTile(m.MapImage, m.BoundsMin, pos, tile)
}
}
m.imageOp = paint.NewImageOp(m.MapImage)
}

View File

@ -0,0 +1,127 @@
package worlds
import (
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/bedrock-tool/bedrocktool/ui/gui"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages"
"github.com/bedrock-tool/bedrocktool/ui/messages"
)
type (
C = layout.Context
D = layout.Dimensions
)
type Page struct {
*pages.Router
worldMap *Map
State messages.UIState
chunkCount int
voidGen bool
worldName string
}
func New(router *pages.Router) *Page {
return &Page{
Router: router,
worldMap: &Map{},
}
}
var _ pages.Page = &Page{}
func (p *Page) Actions() []component.AppBarAction {
return []component.AppBarAction{}
}
func (p *Page) Overflow() []component.OverflowAction {
return []component.OverflowAction{}
}
func (p *Page) NavItem() component.NavItem {
return component.NavItem{
Name: "World Downloader",
//Icon: icon.OtherIcon,
}
}
func (p *Page) Layout(gtx C, th *material.Theme) D {
margin := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
switch p.State {
case messages.UIStateConnect:
// display login page
return margin.Layout(gtx, material.Label(th, 100, "connect Client").Layout)
case messages.UIStateConnecting:
// display connecting to server
return margin.Layout(gtx, material.Label(th, 100, "Connecting").Layout)
case messages.UIStateMain:
// show the main ui
return margin.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Label(th, 20, "World Downloader Basic UI").Layout),
layout.Flexed(1, func(gtx C) D {
return layout.Center.Layout(gtx, p.worldMap.Layout)
}),
)
})
}
return layout.Flex{}.Layout(gtx)
}
func (u *Page) handler(name string, data interface{}) messages.MessageResponse {
r := messages.MessageResponse{
Ok: false,
Data: nil,
}
switch name {
case messages.SetUIState:
state := data.(messages.UIState)
u.State = state
u.Router.Invalidate()
r.Ok = true
case messages.Init:
init := data.(messages.InitPayload)
_ = init
r.Ok = true
case messages.UpdateMap:
update_map := data.(messages.UpdateMapPayload)
u.chunkCount = update_map.ChunkCount
u.worldMap.Update(&update_map)
u.Router.Invalidate()
r.Ok = true
case messages.SetVoidGen:
set_void_gen := data.(messages.SetVoidGenPayload)
u.voidGen = set_void_gen.Value
u.Router.Invalidate()
r.Ok = true
case messages.SetWorldName:
set_world_name := data.(messages.SetWorldNamePayload)
u.worldName = set_world_name.WorldName
u.Router.Invalidate()
r.Ok = true
}
return r
}
func (p *Page) Handler() gui.HandlerFunc {
return p.handler
}

View File

@ -0,0 +1,14 @@
package settings
import (
"gioui.org/layout"
"gioui.org/widget/material"
)
type SettingsUI interface {
Init()
Apply()
Layout(layout.Context, *material.Theme) layout.Dimensions
}
var Settings = map[string]SettingsUI{}

44
ui/gui/settings/skins.go Normal file
View File

@ -0,0 +1,44 @@
package settings
import (
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/bedrock-tool/bedrocktool/subcommands/skins"
"github.com/bedrock-tool/bedrocktool/utils"
)
type skinsSettings struct {
skins *skins.SkinCMD
Filter widget.Editor
Proxy widget.Bool
serverAddress widget.Editor
}
func (s *skinsSettings) Init() {
s.skins = utils.ValidCMDs["skins"].(*skins.SkinCMD)
s.serverAddress.SingleLine = true
s.Filter.SingleLine = true
s.Proxy.Value = true
}
func (s *skinsSettings) Apply() {
s.skins.Filter = s.Filter.Text()
s.skins.NoProxy = !s.Proxy.Value
s.skins.ServerAddress = s.serverAddress.Text()
}
func (s *skinsSettings) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.CheckBox(th, &s.Proxy, "Enable Proxy").Layout),
layout.Rigid(material.Editor(th, &s.Filter, "Player name filter").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(15)}.Layout),
layout.Rigid(material.Editor(th, &s.serverAddress, "server Address").Layout),
)
}
func init() {
Settings["skins"] = &skinsSettings{}
}

44
ui/gui/settings/worlds.go Normal file
View File

@ -0,0 +1,44 @@
package settings
import (
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/bedrock-tool/bedrocktool/subcommands/world"
"github.com/bedrock-tool/bedrocktool/utils"
)
type worldSettings struct {
worlds *world.WorldCMD
withPacks widget.Bool
voidGen widget.Bool
saveImage widget.Bool
serverAddress widget.Editor
}
func (s *worldSettings) Init() {
s.worlds = utils.ValidCMDs["worlds"].(*world.WorldCMD)
s.serverAddress.SingleLine = true
s.voidGen.Value = true
}
func (s *worldSettings) Apply() {
s.worlds.Packs = s.withPacks.Value
s.worlds.EnableVoid = s.voidGen.Value
s.worlds.SaveImage = s.saveImage.Value
s.worlds.ServerAddress = s.serverAddress.Text()
}
func (s *worldSettings) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.CheckBox(th, &s.withPacks, "with Packs").Layout),
layout.Rigid(material.CheckBox(th, &s.voidGen, "void Generator").Layout),
layout.Rigid(material.CheckBox(th, &s.saveImage, "save image").Layout),
layout.Rigid(material.Editor(th, &s.serverAddress, "server Address").Layout),
)
}
func init() {
Settings["worlds"] = &worldSettings{}
}

11
ui/gui/uis.go Normal file
View File

@ -0,0 +1,11 @@
package gui
import (
"gioui.org/layout"
"github.com/bedrock-tool/bedrocktool/ui/messages"
)
type C = layout.Context
type D = layout.Dimensions
type HandlerFunc = func(name string, data interface{}) messages.MessageResponse

74
ui/messages/messages.go Normal file
View File

@ -0,0 +1,74 @@
package messages
import (
"image"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type MessageResponse struct {
Ok bool
Data interface{}
}
type UIState = int
const (
UIStateConnect = iota
UIStateConnecting
UIStateMain
)
type HandlerFunc = func(name string, data interface{}) MessageResponse
//
const SetUIState = "set_ui_state"
type SetUIStatePayload = UIState
//
const SetVoidGen = "set_void_gen"
type SetVoidGenPayload struct {
Value bool
}
//
const SetWorldName = "set_world_name"
type SetWorldNamePayload struct {
WorldName string
}
//
var Init = "init"
type InitPayload struct {
Handler HandlerFunc
}
//
var UpdateMap = "update_map"
type UpdateMapPayload struct {
ChunkCount int
Rotation float32
UpdatedTiles []protocol.ChunkPos
Tiles map[protocol.ChunkPos]*image.RGBA
BoundsMin protocol.ChunkPos
BoundsMax protocol.ChunkPos
}
//
var NewSkin = "new_skin"
type NewSkinPayload struct {
PlayerName string
Skin *protocol.Skin
}

1
ui/stub.go Normal file
View File

@ -0,0 +1 @@
package ui

View File

@ -2,57 +2,64 @@ package utils
import (
"encoding/json"
"fmt"
"os"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/sandertv/gophertunnel/minecraft/auth"
"github.com/sandertv/gophertunnel/minecraft/realms"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
const TOKEN_FILE = "token.json"
const TokenFile = "token.json"
var G_token_src oauth2.TokenSource
var gTokenSrc oauth2.TokenSource
func GetTokenSource() oauth2.TokenSource {
if G_token_src != nil {
return G_token_src
if gTokenSrc != nil {
return gTokenSrc
}
token := get_token()
G_token_src = auth.RefreshTokenSource(&token)
new_token, err := G_token_src.Token()
token := getToken()
gTokenSrc = auth.RefreshTokenSource(&token)
newToken, err := gTokenSrc.Token()
if err != nil {
panic(err)
}
if !token.Valid() {
logrus.Info("Refreshed token")
write_token(new_token)
logrus.Info(locale.Loc("refreshed_token", nil))
writeToken(newToken)
}
return G_token_src
return gTokenSrc
}
var G_realms_api *realms.Client
var RealmsEnv string
func GetRealmsApi() *realms.Client {
if G_realms_api == nil {
G_realms_api = realms.NewClient(GetTokenSource())
var gRealmsAPI *realms.Client
func GetRealmsAPI() *realms.Client {
if gRealmsAPI == nil {
if RealmsEnv != "" {
realms.RealmsAPIBase = fmt.Sprintf("https://pocket-%s.realms.minecraft.net/", RealmsEnv)
}
gRealmsAPI = realms.NewClient(GetTokenSource())
}
return G_realms_api
return gRealmsAPI
}
func write_token(token *oauth2.Token) {
func writeToken(token *oauth2.Token) {
buf, err := json.Marshal(token)
if err != nil {
panic(err)
}
os.WriteFile(TOKEN_FILE, buf, 0o755)
os.WriteFile(TokenFile, buf, 0o755)
}
func get_token() oauth2.Token {
func getToken() oauth2.Token {
var token oauth2.Token
if _, err := os.Stat(TOKEN_FILE); err == nil {
f, err := os.Open(TOKEN_FILE)
if _, err := os.Stat(TokenFile); err == nil {
f, err := os.Open(TokenFile)
if err != nil {
panic(err)
}
@ -65,7 +72,7 @@ func get_token() oauth2.Token {
if err != nil {
panic(err)
}
write_token(_token)
writeToken(_token)
token = *_token
}
return token

View File

@ -0,0 +1,24 @@
package behaviourpack
import (
"github.com/df-mc/dragonfly/server/world"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type blockBehaviour struct {
FormatVersion string `json:"format_version"`
MinecraftBlock world.MinecraftBlock `json:"minecraft:block"`
}
func (bp *BehaviourPack) AddBlock(block protocol.BlockEntry) {
ns, _ := ns_name_split(block.Name)
if ns == "minecraft" {
return
}
entry := blockBehaviour{
FormatVersion: bp.formatVersion,
MinecraftBlock: world.ParseBlock(block),
}
bp.blocks = append(bp.blocks, entry)
}

142
utils/behaviourpack/bp.go Normal file
View File

@ -0,0 +1,142 @@
package behaviourpack
import (
"archive/zip"
"encoding/json"
"os"
"path"
"strings"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
)
type BehaviourPack struct {
formatVersion string
Manifest *resource.Manifest
blocks []blockBehaviour
items map[string]itemBehaviour
entities map[string]entityBehaviour
}
func New(name string) *BehaviourPack {
return &BehaviourPack{
formatVersion: "1.16.0",
Manifest: &resource.Manifest{
FormatVersion: 2,
Header: resource.Header{
Name: name,
Description: "Adds Blocks, Items and Entities from the server to this world",
UUID: utils.RandSeededUUID(name + "_datapack"),
Version: [3]int{1, 0, 0},
MinimumGameVersion: [3]int{1, 19, 50},
},
Modules: []resource.Module{
{
Type: "data",
UUID: utils.RandSeededUUID(name + "_data_module"),
Description: "Datapack",
Version: [3]int{1, 0, 0},
},
},
Dependencies: []resource.Dependency{},
Capabilities: []resource.Capability{},
},
blocks: []blockBehaviour{},
items: make(map[string]itemBehaviour),
entities: make(map[string]entityBehaviour),
}
}
func (bp *BehaviourPack) AddDependency(id string, ver [3]int) {
bp.Manifest.Dependencies = append(bp.Manifest.Dependencies, resource.Dependency{
UUID: id,
Version: ver,
})
}
func (bp *BehaviourPack) CheckAddLink(pack utils.Pack) {
z, err := zip.NewReader(pack, int64(pack.Len()))
if err != nil {
logrus.Error(err)
return
}
if len(bp.blocks) > 0 {
_, err = z.Open("blocks.json")
if err != nil {
return
}
}
h := pack.Manifest().Header
bp.AddDependency(h.UUID, h.Version)
}
func (bp *BehaviourPack) HasBlocks() bool {
return len(bp.blocks) > 0
}
func (bp *BehaviourPack) HasItems() bool {
return len(bp.items) > 0
}
func (bp *BehaviourPack) HasContent() bool {
return bp.HasBlocks() || bp.HasItems()
}
func ns_name_split(identifier string) (ns, name string) {
ns_name := strings.Split(identifier, ":")
return ns_name[0], ns_name[len(ns_name)-1]
}
func (bp *BehaviourPack) Save(fpath string) error {
if err := utils.WriteManifest(bp.Manifest, fpath); err != nil {
return err
}
_add_thing := func(base, identifier string, thing any) error {
ns, name := ns_name_split(identifier)
thing_dir := path.Join(base, ns)
os.Mkdir(thing_dir, 0o755)
w, err := os.Create(path.Join(thing_dir, name+".json"))
if err != nil {
return err
}
e := json.NewEncoder(w)
e.SetIndent("", "\t")
return e.Encode(thing)
}
if len(bp.blocks) > 0 { // blocks
blocks_dir := path.Join(fpath, "blocks")
os.Mkdir(blocks_dir, 0o755)
for _, be := range bp.blocks {
err := _add_thing(blocks_dir, be.MinecraftBlock.Description.Identifier, be)
if err != nil {
return err
}
}
}
if len(bp.items) > 0 { // items
items_dir := path.Join(fpath, "items")
os.Mkdir(items_dir, 0o755)
for _, ib := range bp.items {
err := _add_thing(items_dir, ib.MinecraftItem.Description.Identifier, ib)
if err != nil {
return err
}
}
}
if len(bp.entities) > 0 { // items
items_dir := path.Join(fpath, "entities")
os.Mkdir(items_dir, 0o755)
for _, eb := range bp.entities {
err := _add_thing(items_dir, eb.MinecraftEntity.Description.Identifier, eb)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,84 @@
package behaviourpack
import (
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type EntityDescription struct {
Identifier string `json:"identifier"`
Spawnable bool `json:"is_spawnable"`
Summonable bool `json:"is_summonable"`
Experimental bool `json:"is_experimental"`
}
type MinecraftEntity struct {
Description EntityDescription `json:"description"`
ComponentGroups map[string]any `json:"component_groups"`
Components map[string]any `json:"components"`
Events map[string]any `json:"events,omitempty"`
}
type entityBehaviour struct {
FormatVersion string `json:"format_version"`
MinecraftEntity MinecraftEntity `json:"minecraft:entity"`
}
type EntityIn struct {
Identifier string
Attr []protocol.AttributeValue
Meta protocol.EntityMetadata
}
func (bp *BehaviourPack) AddEntity(entity EntityIn) {
ns, _ := ns_name_split(entity.Identifier)
if ns == "minecraft" {
return
}
if _, ok := bp.entities[entity.Identifier]; ok {
return
}
entry := entityBehaviour{
FormatVersion: bp.formatVersion,
MinecraftEntity: MinecraftEntity{
Description: EntityDescription{
Identifier: entity.Identifier,
Spawnable: true,
Summonable: true,
Experimental: true,
},
ComponentGroups: make(map[string]any),
Components: make(map[string]any),
Events: nil,
},
}
for _, av := range entity.Attr {
switch av.Name {
case "minecraft:health":
entry.MinecraftEntity.Components["minecraft:health"] = map[string]int{
"value": int(av.Value),
"max": int(av.Max),
}
case "minecraft:movement":
entry.MinecraftEntity.Components["minecraft:movement"] = map[string]any{
"value": av.Value,
}
}
}
if scale, ok := entity.Meta[protocol.EntityDataKeyScale].(float32); ok {
entry.MinecraftEntity.Components["minecraft:scale"] = map[string]any{
"value": scale,
}
}
hasCollision := entity.Meta.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagHasCollision)
hasGravity := entity.Meta.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagHasGravity)
entry.MinecraftEntity.Components["minecraft:physics"] = map[string]any{
"has_collision": hasCollision,
"has_gravity": hasGravity,
}
bp.entities[entity.Identifier] = entry
}

View File

@ -0,0 +1,48 @@
package behaviourpack
import "github.com/sandertv/gophertunnel/minecraft/protocol"
type itemDescription struct {
Category string `json:"category"`
Identifier string `json:"identifier"`
IsExperimental bool `json:"is_experimental"`
}
type minecraftItem struct {
Description itemDescription `json:"description"`
Components map[string]any `json:"components,omitempty"`
}
type itemBehaviour struct {
FormatVersion string `json:"format_version"`
MinecraftItem minecraftItem `json:"minecraft:item"`
}
func (bp *BehaviourPack) AddItem(item protocol.ItemEntry) {
ns, _ := ns_name_split(item.Name)
if ns == "minecraft" {
return
}
entry := itemBehaviour{
FormatVersion: bp.formatVersion,
MinecraftItem: minecraftItem{
Description: itemDescription{
Identifier: item.Name,
IsExperimental: true,
},
Components: make(map[string]any),
},
}
bp.items[item.Name] = entry
}
func (bp *BehaviourPack) ApplyComponentEntries(entries []protocol.ItemComponentEntry) {
for _, ice := range entries {
item, ok := bp.items[ice.Name]
if !ok {
continue
}
item.MinecraftItem.Components = ice.Data
}
}

View File

@ -1,10 +1,42 @@
package utils
import "github.com/google/subcommands"
import (
"context"
"flag"
var ValidCMDs = make(map[string]string, 0)
"github.com/google/subcommands"
"github.com/sirupsen/logrus"
)
func RegisterCommand(sub subcommands.Command) {
subcommands.Register(sub, "")
ValidCMDs[sub.Name()] = sub.Synopsis()
var ValidCMDs = make(map[string]Command, 0)
type Command interface {
Name() string
Synopsis() string
SetFlags(f *flag.FlagSet)
Execute(ctx context.Context, ui UI) error
}
type cmdWrap struct {
subcommands.Command
cmd Command
}
func (c *cmdWrap) Name() string { return c.cmd.Name() }
func (c *cmdWrap) Synopsis() string { return c.cmd.Synopsis() }
func (c *cmdWrap) SetFlags(f *flag.FlagSet) { c.cmd.SetFlags(f) }
func (c *cmdWrap) Usage() string { return c.Name() + ": " + c.Synopsis() }
func (c *cmdWrap) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
err := c.cmd.Execute(ctx, CurrentUI)
if err != nil {
logrus.Error(err)
return 1
}
return 0
}
func RegisterCommand(sub Command) {
subcommands.Register(&cmdWrap{cmd: sub}, "")
ValidCMDs[sub.Name()] = sub
}

50
utils/crypt/crypt.go Normal file
View File

@ -0,0 +1,50 @@
package crypt
import (
"bytes"
_ "embed"
"io"
"time"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/packet"
)
//go:embed key.gpg
var key_gpg []byte
var recipients []*openpgp.Entity
func init() {
block, err := armor.Decode(bytes.NewBuffer(key_gpg))
if err != nil {
panic(err)
}
recip, err := openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
panic(err)
}
recipients = append(recipients, recip)
}
func Enc(name string, data []byte) ([]byte, error) {
w := bytes.NewBuffer(nil)
wc, err := openpgp.Encrypt(w, recipients, nil, &openpgp.FileHints{
IsBinary: true, FileName: name, ModTime: time.Now(),
}, nil)
if err != nil {
return nil, err
}
if _, err = wc.Write(data); err != nil {
return nil, err
}
wc.Close()
return w.Bytes(), nil
}
func Encer(name string, w io.Writer) (io.WriteCloser, error) {
wc, err := openpgp.Encrypt(w, recipients, nil, &openpgp.FileHints{
IsBinary: true, FileName: name, ModTime: time.Now(),
}, nil)
return wc, err
}

41
utils/crypt/key.gpg Normal file
View File

@ -0,0 +1,41 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGHqFb8BDADdhaCCihGEuMNtgHo931t2/H6D6I/j+kYZkgt54XeQqTQVLkaW
FzL/MkLB5Hu3CQl1BlCdg8wRyOLdLOyhwtiAiFGcmUel28eIM+Y/Hcr+cUErGrEd
M3r2VjCLytgQeX+jip/Wu/xVrizsCwuy4oxJN1DtAuUMNjP78TBdIa9MJfc68NTW
YAbvG6XT3CJaTHQwWGMUC500Jtg647aRMCdWS1JXQqZiIdqFy/dFCgpfRycg9vpz
RbxQ+EDZ23GDljIcDeaCJ6WWBMIw2pSdTA6psGl4FMeGfHGWaMPudpm5AATG/X1W
bKzhs4JEDcXwu6Si65j7I6BiaxuARfBgRnLBOPhAt+TE0K6jzUcIQHMeLBWk1rfw
wP7hK76OGQiLY4BvpMWyuNXGHy3Le1uePknNBVZppusSgfvm7TUytBg7RfWvw2Dr
j+N07p6KbAn6NelewsybCyeh0k94FvaAtjodjDo1sIogiqLYWleAH/IAF2jFREq6
oGbix9d5UBX+SNEAEQEAAbQdb2xlYmVjayA8b2xlYmVja0BvbGViZWNrLmNvbT6J
Ac4EEwEIADgWIQSGV2qyDgjKdVHooj7U/i4We6MHnQUCYeoVvwIbAwULCQgHAgYV
CgkICwIEFgIDAQIeAQIXgAAKCRDU/i4We6MHnbEzDACrA05J9HvBiQSNXRuVcgT3
5I5brahyhtGdGbbmanvYBNdABkmXsXcVErF4hYASIkQfSZ0uui0kOnmsQRjoKuIX
+agp5d9/S67gwdkafPjkj/vBtCmpFdoNoe24njvlNY0Wd6dYhE0jqCk95ZX0E+AR
J3L1t+f9uFB5GfyVU9oBpSXqZsJD4AoDa7nPz5vNVm3cxPKivlXv1Q5HV96Ngk8R
6Y+hIk8vF5YJ5Q7HLf4xAzqHgbNo8IkbsPAg1uiLozo6bQD0vh8ash9bBycmWujl
jKpDitqPRjNsOhXQ322v90QR2s9mHRyJdi4duSXWPCKlsoTNL4o5Kd/AX/qR1hB8
4Ml7rTI0LDUI5vf2K9p2lxD45ZwJyI8VrORvzjdQcddvtJge6MVQyRktrzEkSeuc
sAjW2xcswgHChmP8f56gLUTZZmAk5TK3A61UkJW8oU0qNBRl4j+Yd84vonW2Dlm2
V20cNmBg7qg2Uldn0TAQrKtzLVQsRp46pFOP6RWcMbO5AY0EYeoVvwEMALo7jQBr
Jtjr2C/abVAmI/ToEif3MPcFv/LkBLuEttTTkDJw9fdj3y/iuqy5EEyG07J8t/+z
PjDLgLUuyNCpxobi80b7GgukDTeiw94ezTyIIXxtb7aYBlHZ/uDecJzNukZ4yNIx
mX/YrUx4isV1APqa66y+eH0aGBZQIZ1sOPeGgsPOVZmIjFDPWEPqBBCQLD96M/Kd
FTRYM8SslnLxBfeX7iye4ZVmLgGyWrJq4cG88Rj++cnp4XI25pJoNgE4GHoQYuep
XazwU8PaJgudQyynIDCrsgDEiCukAY7ZoiVmLSmxWVNNNAdXYKWW88XUMZ0raSSB
H9AsPK4c2YSd916E93nbL3TSiYkVt3Ahty9VAwThoYHZ4Z8/ddD5t6HRTR+/tZn4
JLMskr2FuCb9lR+jOGCp3jW7UezX6G3SLJFB/afBqjhPhurm8Dz63psxIsUZPMzb
yzDXXMjo5lfe1yNXs/joHq4ni74ASpSheO6Kigj+N29hQEZ5AvgkBke/vwARAQAB
iQG2BBgBCAAgFiEEhldqsg4IynVR6KI+1P4uFnujB50FAmHqFb8CGwwACgkQ1P4u
FnujB52j3gwAyO7tBmpNy2NF+LumtUhsB8QYKAs2Xwo7WNQMdYkKrFMD3umXI6n2
BpnfpoJWKeA9HOwJZwdaEggvzcZw2/KPLOW0L2XEOLMoDsJMLQkfaw9ewG9A//em
E0RzTXP1vdbIVdjbNsNmfGa5MiniNDt0khiOkC6u/IXu767vTrVxQwwBvbj/Jhjz
amCuwdFDl4SsadsCm8amYKRFi9k9j2jkYRJSy3KomG7b+2ZfUbmdJoL+NnIivVFZ
AN6WpWhC0Usxgm5xjLLi5f0DlnkOIdPiq8oajA7iCuCGoJvYMZddGigdRdJCZmaR
h19ELdyh/t21ySGCOckkDNQ4cGcm4mm8hilHkJDNsTJ/ACQFjzNyg9mwZ+80fjFa
Wnl6U3bkPsUJsI5vfgVb2td51mxEe6DYd5MTDiKkchlbO+J3vQ0FOb6xkuVSJp0W
ZNl7HmnETLuKjemNoW8Gj0IB0AipLwrisORbYpee1mN2YDasr+0cK5ADIuBXvx6f
cNMCuTrO1l7m
=AZpZ
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -4,11 +4,12 @@ import (
"fmt"
"net"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/miekg/dns"
"github.com/sirupsen/logrus"
)
var override_dns = map[string]bool{
var overrideDNS = map[string]bool{
"geo.hivebedrock.network.": true,
}
@ -23,15 +24,15 @@ func (d *DNSServer) answerQuery(remote net.Addr, req *dns.Msg) (reply *dns.Msg)
case dns.TypeA:
logrus.Infof("Query for %s", q.Name)
if override_dns[q.Name] {
if overrideDNS[q.Name] {
host, _, _ := net.SplitHostPort(remote.String())
remote_ip := net.ParseIP(host)
remoteIP := net.ParseIP(host)
addrs, _ := net.InterfaceAddrs()
var ip string
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.Contains(remote_ip) {
if ipnet.Contains(remoteIP) {
ip = ipnet.IP.String()
}
}
@ -83,17 +84,20 @@ func (d *DNSServer) handler(w dns.ResponseWriter, req *dns.Msg) {
}
func InitDNS() {
if !Options.EnableDNS {
return
}
d := DNSServer{}
dns.HandleFunc(".", d.handler)
server := &dns.Server{Addr: ":53", Net: "udp"}
go func() {
logrus.Infof("Starting dns at %s:53\n", GetLocalIP())
logrus.Infof(locale.Loc("starting_dns", locale.Strmap{"Ip": GetLocalIP()}))
err := server.ListenAndServe()
defer server.Shutdown()
if err != nil {
logrus.Warnf("Failed to start dns server: %s\n ", err.Error())
logrus.Info("you may have to use bedrockconnect")
logrus.Warnf(locale.Loc("failed_to_start_dns", locale.Strmap{"Err": err.Error()}))
logrus.Info(locale.Loc("suggest_bedrockconnect", nil))
}
}()
}

View File

@ -3,6 +3,8 @@ package utils
import (
"image"
"image/color"
"image/png"
"os"
"reflect"
"unsafe"
)
@ -14,6 +16,18 @@ func Img2rgba(img *image.RGBA) []color.RGBA {
return *(*[]color.RGBA)(unsafe.Pointer(&header))
}
func loadPng(path string) (*image.RGBA, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
img, err := png.Decode(f)
if err != nil {
return nil, err
}
return (*image.RGBA)(img.(*image.NRGBA)), err
}
// LERP is a linear interpolation function
func LERP(p1, p2, alpha float64) float64 {
return (1-alpha)*p1 + alpha*p2
@ -34,3 +48,17 @@ func BlendColors(c1, c2 color.RGBA) (ret color.RGBA) {
ret.A = blendAlphaValue(c1.A, c2.A)
return ret
}
// DrawImgScaledPos draws src onto dst at bottomLeft, scaled to size
func DrawImgScaledPos(dst *image.RGBA, src *image.RGBA, bottomLeft image.Point, sizeScaled int) {
sbx := src.Bounds().Dx()
ratio := int(float64(sbx) / float64(sizeScaled))
for xOut := bottomLeft.X; xOut < bottomLeft.X+sizeScaled; xOut++ {
for yOut := bottomLeft.Y; yOut < bottomLeft.Y+sizeScaled; yOut++ {
xIn := (xOut - bottomLeft.X) * ratio
yIn := (yOut - bottomLeft.Y) * ratio
dst.Set(xOut, yOut, src.At(xIn, yIn))
}
}
}

View File

@ -9,10 +9,11 @@ import (
"regexp"
"strings"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/sirupsen/logrus"
)
func User_input(ctx context.Context, q string) (string, bool) {
func UserInput(ctx context.Context, q string) (string, bool) {
c := make(chan string)
go func() {
fmt.Print(q)
@ -31,47 +32,65 @@ func User_input(ctx context.Context, q string) (string, bool) {
}
}
func server_url_to_name(server string) string {
func serverGetHostname(server string) string {
host, _, err := net.SplitHostPort(server)
if err != nil {
logrus.Fatalf("Invalid server: %s", err)
logrus.Fatalf(locale.Loc("invalid_server", locale.Strmap{"Err": err.Error()}))
}
return host
}
func ServerInput(ctx context.Context, server string) (address, name string, err error) {
if server == "" { // no arg provided, interactive input
var (
realmRegex = regexp.MustCompile("realm:(?P<Name>.*)(?::(?P<ID>.*))?")
pcapRegex = regexp.MustCompile(`(?P<Filename>(?P<Name>.*)\.pcap2)(?:\?(?P<Args>.*))?`)
)
func regexGetParams(r *regexp.Regexp, s string) (params map[string]string) {
match := r.FindStringSubmatch(s)
params = make(map[string]string)
for i, name := range r.SubexpNames() {
if i > 0 && i <= len(match) {
params[name] = match[i]
}
}
return params
}
func ServerInput(ctx context.Context, server string) (string, string, error) {
// no arg provided, interactive input
if server == "" {
var cancelled bool
server, cancelled = User_input(ctx, "Enter Server: ")
server, cancelled = UserInput(ctx, locale.Loc("enter_server", nil))
if cancelled {
return "", "", context.Canceled
}
}
if strings.HasPrefix(server, "realm:") { // for realms use api to get ip address
realm_info := strings.Split(server, ":")
id := ""
if len(realm_info) == 3 {
id = realm_info[2]
}
name, address, err = get_realm(context.Background(), GetRealmsApi(), realm_info[1], id)
// realm
if realmRegex.MatchString(server) {
p := regexGetParams(realmRegex, server)
name, address, err := getRealm(ctx, p["Name"], p["ID"])
if err != nil {
return "", "", err
}
name = CleanupName(name)
} else if strings.HasSuffix(server, ".pcap") || strings.HasSuffix(server, ".pcap2") {
s := strings.Split(server, ".")
name = strings.Join(s[:len(s)-1], ".")
address = server
} else {
// if an actual server address if given
// add port if necessary
address = server
if len(strings.Split(address, ":")) == 1 {
address += ":19132"
}
name = server_url_to_name(address)
return address, CleanupName(name), nil
}
return address, name, nil
// old pcap format
if match, _ := regexp.MatchString(`.*\.pcap$`, server); match {
return "", "", fmt.Errorf(locale.Loc("not_supported_anymore", nil))
}
// new pcap format
if pcapRegex.MatchString(server) {
p := regexGetParams(pcapRegex, server)
return "PCAP!" + p["Filename"], p["Name"], nil
}
// normal server dns or ip
if len(strings.Split(server, ":")) == 1 {
server += ":19132"
}
return server, serverGetHostname(server), nil
}

91
utils/iui.go Normal file
View File

@ -0,0 +1,91 @@
package utils
import (
"bufio"
"context"
"flag"
"fmt"
"os"
"strings"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/google/subcommands"
"github.com/sirupsen/logrus"
)
type UI interface {
Init() bool
Start(context.Context, context.CancelFunc) error
Message(name string, data interface{}) messages.MessageResponse
ServerInput(context.Context, string) (string, string, error)
}
type BaseUI struct {
UI
}
func (u *BaseUI) Message(name string, data interface{}) messages.MessageResponse {
return messages.MessageResponse{
Ok: false,
Data: nil,
}
}
func (u *BaseUI) ServerInput(ctx context.Context, server string) (string, string, error) {
address, name, err := ServerInput(ctx, server)
return address, name, err
}
var CurrentUI UI
func SetCurrentUI(ui UI) {
CurrentUI = ui
}
type InteractiveCLI struct {
BaseUI
}
func (c *InteractiveCLI) Init() bool {
CurrentUI = c
return true
}
func (c *InteractiveCLI) Start(ctx context.Context, cancel context.CancelFunc) error {
select {
case <-ctx.Done():
return nil
default:
fmt.Println(locale.Loc("available_commands", nil))
for name, cmd := range ValidCMDs {
fmt.Printf("\t%s\t%s\n", name, cmd.Synopsis())
}
fmt.Println(locale.Loc("use_to_run_command", nil))
cmd, cancelled := UserInput(ctx, locale.Loc("input_command", nil))
if cancelled {
cancel()
return nil
}
_cmd := strings.Split(cmd, " ")
os.Args = append(os.Args, _cmd...)
}
flag.Parse()
InitDNS()
InitExtraDebug(ctx)
subcommands.Execute(ctx)
if Options.IsInteractive {
logrus.Info(locale.Loc("enter_to_exit", nil))
input := bufio.NewScanner(os.Stdin)
input.Scan()
}
return nil
}
var MakeGui = func() UI {
return &InteractiveCLI{}
}

View File

@ -17,7 +17,7 @@ var PrivateIPNetworks = []net.IPNet{
},
}
// check if ip is private
// IPPrivate checks if ip is private
func IPPrivate(ip net.IP) bool {
for _, ipNet := range PrivateIPNetworks {
if ipNet.Contains(ip) {

View File

@ -2,9 +2,15 @@ package utils
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"reflect"
"strings"
"sync"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/fatih/color"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
@ -12,7 +18,7 @@ import (
"golang.org/x/exp/slices"
)
var Pool = packet.NewPool()
var pool = packet.NewPool()
var MutedPackets = []string{
"packet.UpdateBlock",
@ -42,52 +48,175 @@ var MutedPackets = []string{
"packet.PlaySound",
}
var ExtraVerbose []string
var (
ExtraVerbose []string
FLog io.Writer
dmpLock sync.Mutex
)
func dmpStruct(level int, inputStruct any, withType bool, isInList bool) (s string) {
tBase := strings.Repeat("\t", level)
if inputStruct == nil {
return "nil"
}
ii := reflect.Indirect(reflect.ValueOf(inputStruct))
typeName := reflect.TypeOf(inputStruct).String()
typeString := ""
if withType {
if slices.Contains([]string{"bool", "string"}, typeName) {
} else {
typeString = typeName + " "
}
}
if strings.HasPrefix(typeName, "protocol.Optional") {
v := ii.MethodByName("Value").Call(nil)
val, set := v[0], v[1]
if !set.Bool() {
s += typeName + " Not Set"
} else {
s += typeName + "{\n" + tBase + "\t"
s += dmpStruct(level+1, val.Interface(), false, false)
s += "\n" + tBase + "}"
}
return
}
if ii.Kind() == reflect.Struct {
if ii.NumField() == 0 {
s += typeName + "{}"
} else {
s += typeName + "{\n"
for i := 0; i < ii.NumField(); i++ {
fieldType := ii.Type().Field(i)
if fieldType.IsExported() {
field := ii.Field(i).Interface()
d := dmpStruct(level+1, field, true, false)
s += tBase + fmt.Sprintf("\t%s = %s\n", fieldType.Name, d)
} else {
s += tBase + " " + fieldType.Name + " (unexported)"
}
}
s += tBase + "}"
}
} else if ii.Kind() == reflect.Slice {
var t reflect.Type
is_elem_struct := false
if ii.Len() > 0 {
e := ii.Index(0)
t = reflect.TypeOf(e.Interface())
is_elem_struct = t.Kind() == reflect.Struct
}
if ii.Len() > 1000 {
s += "[<slice too long>]"
} else if ii.Len() == 0 {
s += typeString + "[]"
} else {
s += typeString + "["
if is_elem_struct {
s += "\n"
}
for i := 0; i < ii.Len(); i++ {
if is_elem_struct {
s += tBase + "\t"
}
s += dmpStruct(level+1, ii.Index(i).Interface(), false, true)
if is_elem_struct {
s += "\n"
} else {
s += " "
}
}
if is_elem_struct {
s += tBase
}
s += "]"
}
} else if ii.Kind() == reflect.Map {
j, err := json.MarshalIndent(ii.Interface(), tBase, "\t")
if err != nil {
s += err.Error()
}
s += string(j)
} else {
is_array := ii.Kind() == reflect.Array
if !isInList && !is_array {
s += typeString
}
s += fmt.Sprintf("%#v", ii.Interface())
}
return s
}
func DumpStruct(data interface{}) {
if FLog == nil {
return
}
FLog.Write([]byte(dmpStruct(0, data, true, false)))
FLog.Write([]byte("\n\n\n"))
}
func PacketLogger(header packet.Header, payload []byte, src, dst net.Addr) {
var pk packet.Packet
if pkFunc, ok := Pool[header.PacketID]; ok {
if pkFunc, ok := pool[header.PacketID]; ok {
pk = pkFunc()
} else {
pk = &packet.Unknown{PacketID: header.PacketID}
}
if pk.ID() == packet.IDRequestNetworkSettings {
ClientAddr = src
}
defer func() {
if recoveredErr := recover(); recoveredErr != nil {
logrus.Errorf("%T: %w", pk, recoveredErr.(error))
logrus.Errorf("%T: %w", pk, recoveredErr)
}
}()
pk.Unmarshal(protocol.NewReader(bytes.NewBuffer(payload), 0))
pk_name := reflect.TypeOf(pk).String()[1:]
if slices.Contains(MutedPackets, pk_name) {
if FLog != nil {
dmpLock.Lock()
defer dmpLock.Unlock()
FLog.Write([]byte(dmpStruct(0, pk, true, false)))
FLog.Write([]byte("\n\n\n"))
}
pkName := reflect.TypeOf(pk).String()[1:]
if slices.Contains(MutedPackets, pkName) {
return
}
switch pk := pk.(type) {
case *packet.Disconnect:
logrus.Infof("Disconnect: %s", pk.Message)
logrus.Infof(locale.Loc("disconnect", locale.Strmap{"Pk": pk}))
}
dir_S2C := color.GreenString("S") + "->" + color.CyanString("C")
dir_C2S := color.CyanString("C") + "->" + color.GreenString("S")
var dir string = dir_S2C
dirS2C := color.GreenString("S") + "->" + color.CyanString("C")
dirC2S := color.CyanString("C") + "->" + color.GreenString("S")
var dir string = dirS2C
if Client_addr != nil {
if src == Client_addr {
dir = dir_C2S
if ClientAddr != nil {
if src == ClientAddr {
dir = dirC2S
}
} else {
src_addr, _, _ := net.SplitHostPort(src.String())
if IPPrivate(net.ParseIP(src_addr)) {
dir = dir_C2S
srcAddr, _, _ := net.SplitHostPort(src.String())
if IPPrivate(net.ParseIP(srcAddr)) {
dir = dirS2C
}
}
logrus.Debugf("%s 0x%02x, %s", dir, pk.ID(), pk_name)
logrus.Debugf("%s 0x%02x, %s", dir, pk.ID(), pkName)
if slices.Contains(ExtraVerbose, pk_name) {
if slices.Contains(ExtraVerbose, pkName) {
logrus.Debugf("%+v", pk)
}
}

View File

@ -2,20 +2,26 @@ package utils
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"strings"
"sync"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
)
var G_disconnect_reason = "Connection lost"
var DisconnectReason = "Connection lost"
type dummyProto struct {
id int32
@ -33,27 +39,127 @@ func (p dummyProto) ConvertFromLatest(pk packet.Packet, _ *minecraft.Conn) []pac
return []packet.Packet{pk}
}
type ProxyContext struct {
Server *minecraft.Conn
Client *minecraft.Conn
Listener *minecraft.Listener
commands map[string]IngameCommand
type (
PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr)
PacketCallback func(pk packet.Packet, proxy *ProxyContext, toServer bool, timeReceived time.Time) (packet.Packet, error)
ClientConnectCallback func(proxy *ProxyContext, hasClient bool)
ConnectCallback func(proxy *ProxyContext, err error) bool
IngameCommand struct {
Exec func(cmdline []string) bool
Cmd protocol.Command
}
)
log *logrus.Logger
type ProxyContext struct {
Server *minecraft.Conn
Client *minecraft.Conn
Listener *minecraft.Listener
commands map[string]IngameCommand
AlwaysGetPacks bool
WithClient bool
IgnoreDisconnect bool
CustomClientData *login.ClientData
// called for every packet
PacketFunc PacketFunc
// called after client connected
OnClientConnect ClientConnectCallback
// called after game started
ConnectCB ConnectCallback
// called on every packet after login
PacketCB PacketCallback
}
type (
PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr)
PacketCallback func(pk packet.Packet, proxy *ProxyContext, toServer bool) (packet.Packet, error)
ConnectCallback func(proxy *ProxyContext)
)
func NewProxy() (*ProxyContext, error) {
p := &ProxyContext{
commands: make(map[string]IngameCommand),
WithClient: true,
IgnoreDisconnect: false,
}
if Options.PathCustomUserData != "" {
if err := p.LoadCustomUserData(Options.PathCustomUserData); err != nil {
return nil, err
}
}
return p, nil
}
func (p *ProxyContext) AddCommand(cmd IngameCommand) {
p.commands[cmd.Cmd.Name] = cmd
}
type CustomClientData struct {
// skin things
CapeFilename string
SkinFilename string
SkinGeometryFilename string
SkinID string
PlayFabID string
PersonaSkin bool
PremiumSkin bool
TrustedSkin bool
ArmSize string
SkinColour string
// misc
IsEditorMode bool
LanguageCode string
DeviceID string
}
func (p *ProxyContext) LoadCustomUserData(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
var customData CustomClientData
err = json.NewDecoder(f).Decode(&customData)
if err != nil {
return err
}
p.CustomClientData = &login.ClientData{
SkinID: customData.SkinID,
PlayFabID: customData.PlayFabID,
PersonaSkin: customData.PersonaSkin,
PremiumSkin: customData.PremiumSkin,
TrustedSkin: customData.TrustedSkin,
ArmSize: customData.ArmSize,
SkinColour: customData.SkinColour,
}
if customData.SkinFilename != "" {
img, err := loadPng(customData.SkinFilename)
if err != nil {
return err
}
p.CustomClientData.SkinData = base64.RawStdEncoding.EncodeToString(img.Pix)
p.CustomClientData.SkinImageWidth = img.Rect.Dx()
p.CustomClientData.SkinImageHeight = img.Rect.Dy()
}
if customData.CapeFilename != "" {
img, err := loadPng(customData.CapeFilename)
if err != nil {
return err
}
p.CustomClientData.CapeData = base64.RawStdEncoding.EncodeToString(img.Pix)
p.CustomClientData.CapeImageWidth = img.Rect.Dx()
p.CustomClientData.CapeImageHeight = img.Rect.Dy()
}
if customData.SkinGeometryFilename != "" {
data, err := os.ReadFile(customData.SkinGeometryFilename)
if err != nil {
return err
}
p.CustomClientData.SkinGeometry = base64.RawStdEncoding.EncodeToString(data)
}
p.CustomClientData.DeviceID = customData.DeviceID
return nil
}
func (p *ProxyContext) SendMessage(text string) {
if p.Client != nil {
@ -73,16 +179,7 @@ func (p *ProxyContext) SendPopup(text string) {
}
}
type IngameCommand struct {
Exec func(cmdline []string) bool
Cmd protocol.Command
}
func (p *ProxyContext) AddCommand(cmd IngameCommand) {
p.commands[cmd.Cmd.Name] = cmd
}
func (p *ProxyContext) CommandHandlerPacketCB(pk packet.Packet, proxy *ProxyContext, toServer bool) (packet.Packet, error) {
func (p *ProxyContext) CommandHandlerPacketCB(pk packet.Packet, proxy *ProxyContext, toServer bool, _ time.Time) (packet.Packet, error) {
switch _pk := pk.(type) {
case *packet.CommandRequest:
cmd := strings.Split(_pk.CommandLine, " ")
@ -105,14 +202,14 @@ func (p *ProxyContext) CommandHandlerPacketCB(pk packet.Packet, proxy *ProxyCont
return pk, nil
}
func proxyLoop(ctx context.Context, proxy *ProxyContext, toServer bool, packetCBs []PacketCallback) error {
func (p *ProxyContext) proxyLoop(ctx context.Context, toServer bool, packetCBs []PacketCallback) error {
var c1, c2 *minecraft.Conn
if toServer {
c1 = proxy.Client
c2 = proxy.Server
c1 = p.Client
c2 = p.Server
} else {
c1 = proxy.Server
c2 = proxy.Client
c1 = p.Server
c2 = p.Client
}
for {
@ -126,16 +223,16 @@ func proxyLoop(ctx context.Context, proxy *ProxyContext, toServer bool, packetCB
}
for _, packetCB := range packetCBs {
pk, err = packetCB(pk, proxy, toServer)
pk, err = packetCB(pk, p, toServer, time.Now())
if err != nil {
return err
}
}
if pk != nil {
if pk != nil && c2 != nil {
if err := c2.WritePacket(pk); err != nil {
if disconnect, ok := errors.Unwrap(err).(minecraft.DisconnectError); ok {
G_disconnect_reason = disconnect.Error()
DisconnectReason = disconnect.Error()
}
return err
}
@ -143,87 +240,99 @@ func proxyLoop(ctx context.Context, proxy *ProxyContext, toServer bool, packetCB
}
}
func NewProxy(log *logrus.Logger) *ProxyContext {
if log == nil {
log = logrus.StandardLogger()
}
return &ProxyContext{
log: log,
commands: make(map[string]IngameCommand),
}
}
var ClientAddr net.Addr
var Client_addr net.Addr
func (p *ProxyContext) Run(ctx context.Context, server_address string) (err error) {
if strings.HasSuffix(server_address, ".pcap") {
return fmt.Errorf("not supported anymore")
}
if strings.HasSuffix(server_address, ".pcap2") {
return create_replay_connection(ctx, p.log, server_address, p.ConnectCB, p.PacketCB)
func (p *ProxyContext) Run(ctx context.Context, serverAddress string) (err error) {
if strings.HasPrefix(serverAddress, "PCAP!") {
return createReplayConnection(ctx, serverAddress[5:], p.ConnectCB, p.PacketCB)
}
GetTokenSource() // ask for login before listening
var packs []*resource.Pack
if G_preload_packs {
p.log.Info("Preloading resourcepacks")
var serverConn *minecraft.Conn
serverConn, err = ConnectServer(ctx, server_address, nil, true, nil)
if err != nil {
err = fmt.Errorf("failed to connect to %s: %s", server_address, err)
return
var cdp *login.ClientData = nil
if p.WithClient {
var packs []*resource.Pack
if Options.Preload {
logrus.Info(locale.Loc("preloading_packs", nil))
var serverConn *minecraft.Conn
serverConn, err = connectServer(ctx, serverAddress, nil, true, nil)
if err != nil {
err = fmt.Errorf(locale.Loc("failed_to_connect", locale.Strmap{"Address": serverAddress, "Err": err}))
return
}
serverConn.Close()
packs = serverConn.ResourcePacks()
logrus.Infof(locale.Locm("pack_count_loaded", locale.Strmap{"Count": len(packs)}, len(packs)))
}
serverConn.Close()
packs = serverConn.ResourcePacks()
p.log.Infof("%d packs loaded", len(packs))
_status := minecraft.NewStatusProvider("Server")
p.Listener, err = minecraft.ListenConfig{
StatusProvider: _status,
ResourcePacks: packs,
AcceptedProtocols: []minecraft.Protocol{
//dummyProto{id: 567, ver: "1.19.60"},
},
}.Listen("raknet", ":19132")
if err != nil {
return err
}
go func() {
<-ctx.Done()
if p.Client != nil {
p.Listener.Disconnect(p.Client, DisconnectReason)
}
p.Listener.Close()
}()
logrus.Infof(locale.Loc("listening_on", locale.Strmap{"Address": p.Listener.Addr()}))
logrus.Infof(locale.Loc("help_connect", nil))
c, err := p.Listener.Accept()
if err != nil {
return err
}
p.Client = c.(*minecraft.Conn)
cd := p.Client.ClientData()
cdp = &cd
}
_status := minecraft.NewStatusProvider("Server")
p.Listener, err = minecraft.ListenConfig{
StatusProvider: _status,
ResourcePacks: packs,
AcceptedProtocols: []minecraft.Protocol{
dummyProto{id: 544, ver: "1.19.20"},
},
}.Listen("raknet", ":19132")
if p.OnClientConnect != nil {
p.OnClientConnect(p, p.WithClient)
}
if p.CustomClientData != nil {
cdp = p.CustomClientData
}
p.Server, err = connectServer(ctx, serverAddress, cdp, p.AlwaysGetPacks, p.PacketFunc)
if err != nil {
return
if p.ConnectCB != nil {
if p.ConnectCB(p, err) {
err = nil
}
}
err = fmt.Errorf(locale.Loc("failed_to_connect", locale.Strmap{"Address": serverAddress, "Err": err}))
return err
}
defer p.Listener.Close()
p.log.Infof("Listening on %s", p.Listener.Addr())
p.log.Infof("Open Minecraft and connect to this computers local ip address to continue")
var c net.Conn
c, err = p.Listener.Accept()
if err != nil {
p.log.Fatal(err)
}
p.Client = c.(*minecraft.Conn)
cd := p.Client.ClientData()
p.Server, err = ConnectServer(ctx, server_address, &cd, false, p.PacketFunc)
if err != nil {
err = fmt.Errorf("failed to connect to %s: %s", server_address, err)
return
}
// spawn and start the game
if err = spawn_conn(ctx, p.Client, p.Server); err != nil {
err = fmt.Errorf("failed to spawn: %s", err)
return
}
defer p.Server.Close()
defer p.Listener.Disconnect(p.Client, G_disconnect_reason)
// spawn and start the game
if err = spawnConn(ctx, p.Client, p.Server); err != nil {
err = fmt.Errorf(locale.Loc("failed_to_spawn", locale.Strmap{"Err": err}))
return err
}
if p.ConnectCB != nil {
p.ConnectCB(p)
if !p.ConnectCB(p, nil) {
return errors.New("Cancelled")
}
}
wg := sync.WaitGroup{}
cbs := []PacketCallback{
p.CommandHandlerPacketCB,
}
var cbs []PacketCallback
cbs = append(cbs, p.CommandHandlerPacketCB)
if p.PacketCB != nil {
cbs = append(cbs, p.PacketCB)
}
@ -232,21 +341,23 @@ func (p *ProxyContext) Run(ctx context.Context, server_address string) (err erro
wg.Add(1)
go func() {
defer wg.Done()
if err := proxyLoop(ctx, p, false, cbs); err != nil {
p.log.Error(err)
if err := p.proxyLoop(ctx, false, cbs); err != nil {
logrus.Error(err)
return
}
}()
// client to server
wg.Add(1)
go func() {
defer wg.Done()
if err := proxyLoop(ctx, p, true, cbs); err != nil {
p.log.Error(err)
return
}
}()
if p.Client != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := p.proxyLoop(ctx, true, cbs); err != nil {
logrus.Error(err)
return
}
}()
}
wg.Wait()
return err

View File

@ -6,18 +6,16 @@ import (
"fmt"
"strings"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft/realms"
"github.com/sirupsen/logrus"
"github.com/bedrock-tool/bedrocktool/locale"
)
func get_realm(ctx context.Context, api *realms.Client, realm_name, id string) (name string, address string, err error) {
realms, err := api.Realms(ctx)
func getRealm(ctx context.Context, realmName, id string) (name string, address string, err error) {
realms, err := GetRealmsAPI().Realms(ctx)
if err != nil {
return "", "", err
}
for _, realm := range realms {
if strings.HasPrefix(realm.Name, realm_name) {
if strings.HasPrefix(realm.Name, realmName) {
if id != "" && id != fmt.Sprint(id) {
continue
}
@ -34,25 +32,18 @@ func get_realm(ctx context.Context, api *realms.Client, realm_name, id string) (
type RealmListCMD struct{}
func (*RealmListCMD) Name() string { return "list-realms" }
func (*RealmListCMD) Synopsis() string { return "prints all realms you have access to" }
func (*RealmListCMD) Name() string { return "list-realms" }
func (*RealmListCMD) Synopsis() string { return locale.Loc("list_realms_synopsis", nil) }
func (c *RealmListCMD) SetFlags(f *flag.FlagSet) {}
func (c *RealmListCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n"
}
func (c *RealmListCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
api := realms.NewClient(GetTokenSource())
realms, err := api.Realms(ctx)
func (c *RealmListCMD) Execute(ctx context.Context, ui UI) error {
realms, err := GetRealmsAPI().Realms(ctx)
if err != nil {
logrus.Error(err)
return 1
return err
}
for _, realm := range realms {
fmt.Printf("Name: %s\tid: %d\n", realm.Name, realm.ID)
fmt.Println(locale.Loc("realm_list_line", locale.Strmap{"Name": realm.Name, "Id": realm.ID}))
}
return 0
return nil
}
func init() {

View File

@ -1,89 +1,136 @@
package utils
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"net"
"os"
"reflect"
"time"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
func create_replay_connection(ctx context.Context, log *logrus.Logger, filename string, onConnect ConnectCallback, packetCB PacketCallback) error {
log.Infof("Reading replay %s", filename)
type replayHeader struct {
Version int32
}
var replayMagic = []byte("BTCP")
const (
currentReplayVersion = 2
)
func WriteReplayHeader(f io.Writer) {
f.Write(replayMagic)
header := replayHeader{
Version: currentReplayVersion,
}
binary.Write(f, binary.LittleEndian, &header)
}
func createReplayConnection(ctx context.Context, filename string, onConnect ConnectCallback, packetCB PacketCallback) error {
logrus.Infof("Reading replay %s", filename)
f, err := os.Open(filename)
if err != nil {
return err
}
var size int64
{
stat, err := f.Stat()
if err != nil {
stat, err := f.Stat()
if err != nil {
return err
}
totalSize := stat.Size()
// default version is version 1, since that didnt have a header
ver := 1
magic := make([]byte, 4)
io.ReadAtLeast(f, magic, 4)
if bytes.Equal(magic, replayMagic) {
var header replayHeader
if err := binary.Read(f, binary.LittleEndian, &header); err != nil {
return err
}
size = stat.Size()
ver = int(header.Version)
} else {
logrus.Info("Version 1 capture assumed.")
f.Seek(-4, io.SeekCurrent)
}
proxy := NewProxy(logrus.StandardLogger())
proxy, _ := NewProxy()
proxy.Server = minecraft.NewConn()
game_started := false
gameStarted := false
i := 0
for {
i += 1
var magic uint32 = 0
var packet_length uint32 = 0
var packetLength uint32 = 0
var toServer bool = false
timeReceived := time.Now()
offset, _ := f.Seek(0, io.SeekCurrent)
if offset == size {
log.Info("Reached End")
if offset == totalSize {
logrus.Info("Reached End")
return nil
}
binary.Read(f, binary.LittleEndian, &magic)
if magic != 0xAAAAAAAA {
logrus.Fatal("Wrong Magic")
return fmt.Errorf("wrong Magic")
}
binary.Read(f, binary.LittleEndian, &packet_length)
binary.Read(f, binary.LittleEndian, &packetLength)
binary.Read(f, binary.LittleEndian, &toServer)
payload := make([]byte, packet_length)
if ver >= 2 {
var timeMs int64
binary.Read(f, binary.LittleEndian, &timeMs)
timeReceived = time.UnixMilli(timeMs)
}
payload := make([]byte, packetLength)
n, err := f.Read(payload)
if err != nil {
log.Error(err)
logrus.Error(err)
return nil
}
if n != int(packet_length) {
log.Errorf("Truncated %d", i)
return nil
if n != int(packetLength) {
return fmt.Errorf("truncated %d", i)
}
var magic2 uint32
binary.Read(f, binary.LittleEndian, &magic2)
if magic2 != 0xBBBBBBBB {
logrus.Fatal("Wrong Magic2")
return fmt.Errorf("wrong Magic2")
}
pk_data, err := minecraft.ParseData(payload, proxy.Server)
pkData, err := minecraft.ParseData(payload, proxy.Server)
if err != nil {
return err
}
pks, err := pk_data.Decode(proxy.Server)
pks, err := pkData.Decode(proxy.Server)
if err != nil {
log.Error(err)
logrus.Error(err)
continue
}
for _, pk := range pks {
logrus.Printf("%s", reflect.TypeOf(pk).String()[1:])
f := bytes.NewBuffer(nil)
b := protocol.NewWriter(f, 0)
pk.Marshal(b)
if game_started {
if Options.Debug {
PacketLogger(packet.Header{PacketID: pk.ID()}, f.Bytes(), &net.UDPAddr{}, &net.UDPAddr{})
}
if gameStarted {
if packetCB != nil {
packetCB(pk, proxy, toServer)
packetCB(pk, proxy, toServer, timeReceived)
}
} else {
switch pk := pk.(type) {
@ -117,9 +164,9 @@ func create_replay_connection(ctx context.Context, log *logrus.Logger, filename
ChatRestrictionLevel: pk.ChatRestrictionLevel,
DisablePlayerInteractions: pk.DisablePlayerInteractions,
})
game_started = true
gameStarted = true
if onConnect != nil {
onConnect(proxy)
onConnect(proxy, nil)
}
}
}

Binary file not shown.

View File

@ -1,14 +1,82 @@
package utils
import (
"errors"
"io"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/resource"
)
type Pack interface {
io.ReaderAt
ReadAll() ([]byte, error)
Decrypt() ([]byte, error)
Encrypted() bool
CanDecrypt() bool
UUID() string
Name() string
Version() string
ContentKey() string
Len() int
Manifest() resource.Manifest
Base() *resource.Pack
}
type Packb struct {
*resource.Pack
}
func (p *Packb) ReadAll() ([]byte, error) {
buf := make([]byte, p.Len())
off := 0
for {
n, err := p.ReadAt(buf[off:], int64(off))
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
off += n
}
return buf, nil
}
func (p *Packb) CanDecrypt() bool {
return false
}
func (p *Packb) Decrypt() ([]byte, error) {
return nil, errors.New("no_decrypt")
}
func (p *Packb) Base() *resource.Pack {
return p.Pack
}
var PackFromBase = func(pack *resource.Pack) Pack {
b := &Packb{pack}
return b
}
func GetPacks(server *minecraft.Conn) (packs map[string]*resource.Pack, err error) {
packs = make(map[string]*resource.Pack)
for _, pack := range server.ResourcePacks() {
packs[pack.Name()] = pack
pack := PackFromBase(pack)
if pack.Encrypted() && pack.CanDecrypt() {
data, err := pack.Decrypt()
if err != nil {
return nil, err
}
pack2, err := resource.FromBytes(data)
if err != nil {
return nil, err
}
packs[pack.Name()] = pack2
} else {
packs[pack.Name()] = pack.Base()
}
}
return
}

View File

@ -1,15 +1,50 @@
package utils
import "github.com/sanbornm/go-selfupdate/selfupdate"
import (
"fmt"
"io"
"net/http"
"os"
"runtime"
"github.com/sanbornm/go-selfupdate/selfupdate"
)
var Version string
var CmdName = "bedrocktool"
const updateServer = "https://updates.yuv.pink/"
type trequester struct {
selfupdate.Requester
}
func (httpRequester *trequester) Fetch(url string) (io.ReadCloser, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// set user agent to know what versions are run
h, _ := os.Hostname()
req.Header.Add("User-Agent", fmt.Sprintf("%s %s '%s' %d", CmdName, Version, h, runtime.NumCPU()))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
}
return resp.Body, nil
}
var Updater = &selfupdate.Updater{
CurrentVersion: Version,
ApiURL: updateServer,
BinURL: updateServer,
Dir: "update/",
CmdName: "bedrocktool", // app name
CmdName: CmdName,
Requester: &trequester{},
}

View File

@ -1,18 +1,23 @@
// Package utils ...
package utils
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"crypto/aes"
"crypto/sha256"
"encoding/json"
"fmt"
"errors"
"io"
"net"
"os"
"path"
"regexp"
"strings"
"sync"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils/crypt"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sirupsen/logrus"
@ -21,34 +26,21 @@ import (
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
)
const SERVER_ADDRESS_HELP = `accepted server address formats:
123.234.123.234
123.234.123.234:19132
realm:<Realmname>
realm:<Realmname>:<Id>
`
var (
G_debug bool
G_preload_packs bool
G_interactive bool
)
var A string
func init() {
b, _ := base64.RawStdEncoding.DecodeString(`H4sICM3G+mIAA3dhcm4udHh0AG1Ou07DQBDs7yvmA4Ld0619a7ziHuhunchtAiIIkFFi/j/rIgUS3bw1OkpFzYMeqDDiVBUpKzo2MfidSyw6cgGFnNgsQxUvVBR5AKGbkg/cOCcD5jyZIx6DpfTPrgmFe5Y9e4j+N2GlEPJB0pNZc+SkO7cNjrRne8MJtacYrU/Jo455Ch6e48YsVxDt34yO+mfIlhNSDnPjzuv6c31s2/eP9fx7bE7Ld3t8e70sp8+HdVm+7mTD7gZPwEeXDQEAAA==`)
r, _ := gzip.NewReader(bytes.NewBuffer(b))
d, _ := io.ReadAll(r)
A = string(d)
var Options struct {
Debug bool
Preload bool
IsInteractive bool
ExtraDebug bool
EnableDNS bool
PathCustomUserData string
}
var name_regexp = regexp.MustCompile(`\||(?:§.?)`)
var nameRegexp = regexp.MustCompile(`\||(?:§.?)`)
// cleans name so it can be used as a filename
// CleanupName cleans name so it can be used as a filename
func CleanupName(name string) string {
name = strings.Split(name, "\n")[0]
var _tmp struct {
@ -58,71 +50,77 @@ func CleanupName(name string) string {
if err == nil {
name = _tmp.K
}
name = string(name_regexp.ReplaceAll([]byte(name), []byte("")))
name = string(nameRegexp.ReplaceAll([]byte(name), []byte("")))
name = strings.TrimSpace(name)
return name
}
// connections
func ConnectServer(ctx context.Context, address string, ClientData *login.ClientData, want_packs bool, packetFunc PacketFunc) (serverConn *minecraft.Conn, err error) {
packet_func := func(header packet.Header, payload []byte, src, dst net.Addr) {
if G_debug {
PacketLogger(header, payload, src, dst)
}
if packetFunc != nil {
packetFunc(header, payload, src, dst)
}
}
func connectServer(ctx context.Context, address string, ClientData *login.ClientData, wantPacks bool, packetFunc PacketFunc) (serverConn *minecraft.Conn, err error) {
cd := login.ClientData{}
if ClientData != nil {
cd = *ClientData
}
logrus.Infof("Connecting to %s\n", address)
logrus.Info(locale.Loc("connecting", locale.Strmap{"Address": address}))
serverConn, err = minecraft.Dialer{
TokenSource: GetTokenSource(),
ClientData: cd,
PacketFunc: packet_func,
DownloadResourcePack: func(id uuid.UUID, version string) bool {
return want_packs
PacketFunc: func(header packet.Header, payload []byte, src, dst net.Addr) {
if Options.Debug {
PacketLogger(header, payload, src, dst)
}
if packetFunc != nil {
packetFunc(header, payload, src, dst)
}
},
DownloadResourcePack: func(id uuid.UUID, version string, current int, total int) bool {
return wantPacks
},
}.DialContext(ctx, "raknet", address)
if err != nil {
return nil, err
return serverConn, err
}
logrus.Debug("Connected.")
Client_addr = serverConn.LocalAddr()
logrus.Debug(locale.Loc("connected", nil))
ClientAddr = serverConn.LocalAddr()
return serverConn, nil
}
func spawn_conn(ctx context.Context, clientConn *minecraft.Conn, serverConn *minecraft.Conn) error {
func spawnConn(ctx context.Context, clientConn *minecraft.Conn, serverConn *minecraft.Conn) error {
wg := sync.WaitGroup{}
errs := make(chan error, 2)
if clientConn != nil {
wg.Add(1)
go func() {
defer wg.Done()
errs <- clientConn.StartGame(serverConn.GameData())
}()
}
wg.Add(1)
go func() {
errs <- clientConn.StartGame(serverConn.GameData())
}()
go func() {
defer wg.Done()
errs <- serverConn.DoSpawn()
}()
// wait for both to finish
wg.Wait()
for i := 0; i < 2; i++ {
select {
case err := <-errs:
if err != nil {
return fmt.Errorf("failed to start game: %s", err)
return errors.New(locale.Loc("failed_start_game", locale.Strmap{"Err": err}))
}
case <-ctx.Done():
return fmt.Errorf("connection cancelled")
return errors.New(locale.Loc("connection_cancelled", nil))
default:
}
}
return nil
}
// get longest line length
func max_len(lines []string) int {
func maxLen(lines []string) int {
o := 0
for _, line := range lines {
if o < len(line) {
@ -132,10 +130,10 @@ func max_len(lines []string) int {
return o
}
// make text centered
// MarginLines makes text centered
func MarginLines(lines []string) string {
ret := ""
max := max_len(lines)
max := maxLen(lines)
for _, line := range lines {
if len(line) != max {
ret += strings.Repeat(" ", max/2-len(line)/4)
@ -163,3 +161,82 @@ func Clamp(a, b int) int {
}
return a
}
func RandSeededUUID(str string) string {
h := sha256.Sum256([]byte(str))
id, _ := uuid.NewRandomFromReader(bytes.NewBuffer(h[:]))
return id.String()
}
func WriteManifest(manifest *resource.Manifest, fpath string) error {
w, err := os.Create(path.Join(fpath, "manifest.json"))
if err != nil {
return err
}
e := json.NewEncoder(w)
e.SetIndent("", "\t")
if err = e.Encode(manifest); err != nil {
return err
}
return nil
}
func CfbDecrypt(data []byte, key []byte) []byte {
cipher, _ := aes.NewCipher([]byte(key))
shiftRegister := append(key[:16], data...)
iv := make([]byte, 16)
off := 0
for ; off < len(data); off += 1 {
cipher.Encrypt(iv, shiftRegister)
data[off] ^= iv[0]
shiftRegister = shiftRegister[1:]
}
return data
}
func InitExtraDebug(ctx context.Context) {
if !Options.ExtraDebug {
return
}
Options.Debug = true
var logPlain, logCryptEnc io.WriteCloser = nil, nil
// open plain text log
logPlain, err := os.Create("packets.log")
if err != nil {
logrus.Error(err)
} else {
go func() {
<-ctx.Done()
logPlain.Close()
}()
}
// open gpg log
logCrypt, err := os.Create("packets.log.gpg")
if err != nil {
logrus.Error(err)
} else {
go func() {
<-ctx.Done()
logCrypt.Close()
}()
// encrypter for the log
logCryptEnc, err = crypt.Encer("packets.log", logCrypt)
if err != nil {
logrus.Error(err)
} else {
go func() {
<-ctx.Done()
logCryptEnc.Close()
}()
}
}
FLog = io.MultiWriter(logPlain, logCryptEnc)
if err != nil {
logrus.Error(err)
}
}

View File

@ -2,25 +2,27 @@ package utils
import (
"archive/zip"
"fmt"
"io"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"strings"
"github.com/sirupsen/logrus"
)
func UnpackZip(r io.ReaderAt, size int64, unpack_folder string) {
func UnpackZip(r io.ReaderAt, size int64, unpackFolder string) {
zr, _ := zip.NewReader(r, size)
for _, src_file := range zr.File {
out_path := path.Join(unpack_folder, src_file.Name)
if src_file.Mode().IsDir() {
os.Mkdir(out_path, 0o755)
for _, srcFile := range zr.File {
srcName := strings.ReplaceAll(srcFile.Name, "\\", "/")
outPath := path.Join(unpackFolder, srcName)
if srcFile.Mode().IsDir() {
os.Mkdir(outPath, 0o755)
} else {
os.MkdirAll(path.Dir(out_path), 0o755)
fr, _ := src_file.Open()
f, _ := os.Create(path.Join(unpack_folder, src_file.Name))
os.MkdirAll(path.Dir(outPath), 0o755)
fr, _ := srcFile.Open()
f, _ := os.Create(path.Join(unpackFolder, srcName))
io.Copy(f, fr)
}
}
@ -29,7 +31,7 @@ func UnpackZip(r io.ReaderAt, size int64, unpack_folder string) {
func ZipFolder(filename, folder string) error {
f, err := os.Create(filename)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
zw := zip.NewWriter(f)
err = filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error {
@ -38,7 +40,7 @@ func ZipFolder(filename, folder string) error {
zwf, _ := zw.Create(rel)
data, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
logrus.Error(err)
}
zwf.Write(data)
}