├── .git-crypt ├── .gitattributes └── keys │ └── default │ └── 0 │ └── 86576AB20E08CA7551E8A23ED4FE2E167BA3079D.gpg ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Readme.md ├── build.go ├── cmd ├── bedrocktool │ ├── cli.go │ ├── console_android.go │ ├── console_linux.go │ ├── console_windows.go │ ├── gui.go │ ├── log.go │ └── main.go ├── dump-actors │ ├── main.go │ └── usage.md ├── generate-color-lookup │ └── main.go └── libbedrocktool │ ├── main.go │ └── msg_cb.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── handlers ├── capture.go ├── chat_log.go ├── skins.go └── worlds │ ├── biome.go │ ├── chunk.go │ ├── entity │ └── entity.go │ ├── map_item.go │ ├── packets.go │ ├── player.go │ ├── scripting │ ├── entitydata.go │ ├── events.go │ └── scripting.go │ ├── world.go │ └── worldstate │ ├── packs.go │ ├── players.go │ ├── world.go │ └── worldstate.go ├── icon.png ├── locale ├── de.yaml ├── en.yaml ├── i18n.go ├── lang.go └── lang_js.go ├── scripting ├── bedrocktool.d.ts ├── example.js ├── jsconfig.json └── scripting-ts │ ├── example.ts │ └── tsconfig.json ├── subcommands ├── capture.go ├── chat_log.go ├── debug.go ├── merge.go ├── realm-address.go ├── realms-list.go ├── render.go ├── resourcepack-d.go ├── resourcepack-d │ └── resourcepack-d.go ├── resourcepack-stub.go ├── skins.go ├── update.go └── worlds.go ├── ui ├── cli │ └── cli.go ├── gui │ ├── gui.go │ ├── guim │ │ └── guim.go │ ├── icons │ │ └── main.go │ ├── logger.go │ ├── mctext │ │ └── mctext.go │ ├── pages │ │ ├── packs │ │ │ └── packs.go │ │ ├── page.go │ │ ├── router.go │ │ ├── settings │ │ │ ├── address-input.go │ │ │ ├── file-input.go │ │ │ ├── settings.go │ │ │ └── settingsPage.go │ │ ├── skins │ │ │ └── skins.go │ │ ├── toast.go │ │ ├── update │ │ │ └── update.go │ │ └── worlds │ │ │ ├── map2.go │ │ │ ├── map2_test.go │ │ │ └── worlds.go │ └── popups │ │ ├── authpopup.go │ │ ├── connect.go │ │ ├── errorpopup.go │ │ ├── gatherings.go │ │ ├── popup.go │ │ ├── realmsinput.go │ │ └── update.go ├── messages │ └── events.go ├── rui │ ├── proto.go │ └── rui.go └── ui.go └── utils ├── auth.go ├── behaviourpack ├── biomes.go ├── block.go ├── bp.go ├── custom_block.go ├── entity.go ├── item.go └── player.json ├── blockcolors.go ├── cfb8_test.go ├── chunk_render.go ├── colors.go ├── commands ├── args.go └── register.go ├── connect_info.go ├── connect_info_test.go ├── crypt ├── crypt.go └── key.gpg ├── discovery ├── authservice.go ├── discovery.go └── gatherings.go ├── dumpstruct.go ├── folders.go ├── fswriter.go ├── images.go ├── input.go ├── invalid_json_test.go ├── merge └── block_registry.go ├── nbtconv ├── colour.go ├── item.go ├── read.go └── write.go ├── net.go ├── netisolation_other.go ├── netisolation_windows.go ├── osabs ├── osabs.go ├── osabs_android.go ├── osabs_linux.go └── osabs_windows.go ├── panic.go ├── playfab ├── playfab.go └── types.go ├── proxy ├── blobcache │ ├── blobcache.go │ ├── blobcache_other.go │ └── blobcache_windows.go ├── context.go ├── handlers.go ├── packet_logger.go ├── pcap2 │ ├── pcap2reader.go │ └── replay_connector.go ├── player.go ├── proxy.go ├── resourcepacks │ ├── packcache.go │ ├── replaycache.go │ └── resourcepacks.go └── session.go ├── recover.go ├── resourcepack └── resourcepack.go ├── rmtree.go ├── rmtree_other.go ├── rmtree_windows.go ├── skinconverter ├── skin.go └── skinpack.go ├── stripansi.go ├── temp_other.go ├── temp_windows.go ├── texture_map.go ├── updater ├── updater.go ├── updater_android.go └── updater_other.go ├── utils.go ├── ver.go └── xbox ├── device_token.go ├── live.go ├── sign.go └── xbox.go /.git-crypt/.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not edit this file. To specify the files to encrypt, create your own 2 | # .gitattributes file in the directory where your files are. 3 | * !filter !diff 4 | *.gpg binary 5 | -------------------------------------------------------------------------------- /.git-crypt/keys/default/0/86576AB20E08CA7551E8A23ED4FE2E167BA3079D.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bedrock-tool/bedrocktool/ff59f55cfa0cd02a4825976dd1b5e13c679b8a9a/.git-crypt/keys/default/0/86576AB20E08CA7551E8A23ED4FE2E167BA3079D.gpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | subcommands/resourcepack-d/resourcepack-d.go filter=git-crypt diff=git-crypt 2 | * eol=lf 3 | *.png binary -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is, 12 | and optionally what server it occurs on. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. windows] 29 | - Version [e.g. 1.28.0-36] 30 | - Minecraft Version [e.g. 1.19.73] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | 35 | 36 | **attach packets.log.gpg (not always necessary)** 37 | this file can be helpful for debugging without having to connect to the server. 38 | it can be created by running with -extra-debug [e.g. bedrocktool.exe -extra-debug worlds -address play.mojang.com ] 39 | be sure to only attach the .gpg file which is not publicly readable. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: ci-build 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - name: Install SSH Key 14 | if: ${{ env.SSH_PRIVATE_KEY != '' }} 15 | uses: shimataro/ssh-key-action@v2 16 | with: 17 | key: ${{ secrets.SSH_PRIVATE_KEY }} 18 | known_hosts: ${{ secrets.KNOWN_HOSTS }} 19 | env: 20 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 21 | 22 | - uses: awalsh128/cache-apt-pkgs-action@latest 23 | if: ${{ env.REPO_KEY != '' }} 24 | with: 25 | 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 26 | version: 1.0 27 | env: 28 | REPO_KEY: ${{ secrets.REPO_KEY }} 29 | 30 | - name: Setup Golang with cache 31 | uses: magnetikonline/action-golang-cache@v3 32 | with: 33 | go-version: ~1.24 34 | 35 | - uses: actions/checkout@v3 36 | with: 37 | submodules: true 38 | fetch-depth: 0 39 | 40 | - run: | 41 | git fetch --force --tags 42 | 43 | - name: decrypt 44 | if: ${{ env.REPO_KEY != '' }} 45 | run: | 46 | echo ${REPO_KEY} | xxd -r -p > ../bedrock-repo-key.key 47 | git status --porcelain 48 | git-crypt unlock ../bedrock-repo-key.key 49 | rm ../bedrock-repo-key.key 50 | env: 51 | REPO_KEY: ${{ secrets.REPO_KEY }} 52 | 53 | - name: dependencies 54 | run: | 55 | go get ./cmd/bedrocktool 56 | go install gioui.org/cmd/gogio@latest 57 | 58 | - name: build 59 | id: build 60 | run: | 61 | go run ./build.go windows,linux gui,cli 62 | 63 | - name: Deploy with rsync 64 | if: ${{ env.SSH_HOST != '' }} 65 | run: rsync -avzO ./updates/ olebeck@${SSH_HOST}:/var/www/updates/ 66 | env: 67 | SSH_HOST: ${{ secrets.SSH_HOST }} 68 | 69 | - name: 🏷️ Create/update tag 70 | uses: actions/github-script@v7 71 | with: 72 | script: | 73 | github.rest.git.createRef({ 74 | owner: context.repo.owner, 75 | repo: context.repo.repo, 76 | ref: 'refs/tags/${{ steps.build.outputs.release_tag }}', 77 | sha: context.sha 78 | }).catch(err => { 79 | if (err.status !== 422) throw err; 80 | github.rest.git.updateRef({ 81 | owner: context.repo.owner, 82 | repo: context.repo.repo, 83 | ref: 'tags/${{ steps.build.outputs.release_tag }}', 84 | sha: context.sha 85 | }); 86 | }) 87 | 88 | - uses: ncipollo/release-action@v1 89 | with: 90 | artifacts: ./builds/* 91 | bodyFile: changelog.txt 92 | removeArtifacts: true 93 | replacesArtifacts: true 94 | allowUpdates: true 95 | makeLatest: ${{ steps.build.outputs.is_latest }} 96 | tag: ${{ steps.build.outputs.release_tag }} 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | token*.json 2 | .secrets 3 | 4 | *.mcpack 5 | *.zip 6 | *.pcap 7 | *.pcap2 8 | *.bmp 9 | *.bin 10 | *.log 11 | *.gpg 12 | *.syso 13 | *.pprof 14 | 15 | /bedrocktool 16 | /bedrocktool-* 17 | __debug_bin* 18 | 19 | .vscode/* 20 | keys.db 21 | /skins/ 22 | /worlds/ 23 | /packs/ 24 | /dist/ 25 | /builds/ 26 | /updates/ 27 | /fyne-cross/ 28 | /tmp/ 29 | /cmd/test 30 | 31 | packets.log.gpg 32 | customdata.json 33 | /other-projects/ 34 | __pycache__/ 35 | gathering.py 36 | *.ipynb 37 | entityflags.py 38 | *.ignore 39 | 40 | /libbedrocktool.h 41 | /libbedrocktool.so 42 | /tex.png 43 | /packcache/* 44 | /blobcache 45 | /*-test 46 | /actors*.txt 47 | changelog.txt 48 | testservers/* 49 | /scripts/ 50 | 51 | actors.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gophertunnel"] 2 | path = gophertunnel 3 | url = https://github.com/olebeck/gophertunnel.git 4 | [submodule "dragonfly"] 5 | path = dragonfly 6 | url = https://github.com/olebeck/dragonfly.git 7 | [submodule "go-raknet"] 8 | path = go-raknet 9 | url = git@github.com:olebeck/go-raknet 10 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # bedrocktool 2 | a minecraft bedrock proxy that can among other things save worlds from servers 3 | 4 |
5 | 6 | ## downloads: 7 | ### [here](https://github.com/bedrock-tool/bedrocktool/releases) 8 | 9 |
10 | 11 | ## issues: 12 | 13 | if you find an issue or a crash, please report it by opening a github issue with a screenshot of the crash or issue, thanks 14 | 15 |
16 | 17 | ``` 18 | Usage: bedrocktool 19 | 20 | Subcommands: 21 | capture capture packets in a pcap file 22 | help describe subcommands and their syntax 23 | list-realms prints all realms you have access to 24 | merge merge 2 or more worlds 25 | packs download resource packs from a server 26 | realms-token print xbl3.0 token for realms api 27 | skins download all skins from players on a server 28 | skins-proxy download skins from players on a server with proxy 29 | worlds download a world from a server 30 | 31 | 32 | Top-level flags (use "bedrocktool flags" for a full list): 33 | -debug=false: debug mode (enables extra logging useful for finding bugs) 34 | -dns=false: enable dns server for consoles (use this if you need to connect on a console) 35 | ``` -------------------------------------------------------------------------------- /cmd/bedrocktool/cli.go: -------------------------------------------------------------------------------- 1 | //go:build !android 2 | 3 | package main 4 | 5 | import "github.com/bedrock-tool/bedrocktool/ui/cli" 6 | 7 | func init() { 8 | uis["cli"] = &cli.CLI{} 9 | } 10 | -------------------------------------------------------------------------------- /cmd/bedrocktool/console_android.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | package main 4 | 5 | import "os" 6 | 7 | func redirectStderr(f *os.File) { 8 | } 9 | -------------------------------------------------------------------------------- /cmd/bedrocktool/console_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux && !android 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | func redirectStderr(f *os.File) { 12 | err := syscall.Dup2(int(f.Fd()), int(os.Stderr.Fd())) 13 | if err != nil { 14 | log.Fatalf("Failed to redirect stderr to file: %v", err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/bedrocktool/console_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | func init() { 12 | stdout := windows.Handle(os.Stdout.Fd()) 13 | var originalMode uint32 14 | 15 | windows.GetConsoleMode(stdout, &originalMode) 16 | windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) 17 | logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true}) 18 | } 19 | 20 | // redirectStderr to the file passed in 21 | func redirectStderr(f *os.File) { 22 | err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(f.Fd())) 23 | if err != nil { 24 | log.Fatalf("Failed to redirect stderr to file: %v", err) 25 | } 26 | // SetStdHandle does not affect prior references to stderr 27 | os.Stderr = f 28 | } 29 | -------------------------------------------------------------------------------- /cmd/bedrocktool/gui.go: -------------------------------------------------------------------------------- 1 | //go:build gui || android 2 | 3 | package main 4 | 5 | import "github.com/bedrock-tool/bedrocktool/ui/gui" 6 | 7 | func init() { 8 | uis["gui"] = &gui.GUI{} 9 | } 10 | -------------------------------------------------------------------------------- /cmd/bedrocktool/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/bedrock-tool/bedrocktool/utils" 9 | "github.com/rifflock/lfshook" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type logFileWriter struct { 14 | w io.Writer 15 | } 16 | 17 | func (l logFileWriter) Write(b []byte) (int, error) { 18 | if utils.LogOff { 19 | return len(b), nil 20 | } 21 | return l.w.Write(b) 22 | } 23 | 24 | func setupLogging(isDebug bool) { 25 | logFile, err := os.Create(utils.PathData("bedrocktool.log")) 26 | if err != nil { 27 | logrus.Fatal(err) 28 | } 29 | 30 | rOut, wOut, err := os.Pipe() 31 | if err != nil { 32 | logrus.Fatal(err) 33 | } 34 | 35 | originalStdout := os.Stdout 36 | logWriter := logFileWriter{w: logFile} 37 | go func() { 38 | m := io.MultiWriter(originalStdout, logWriter) 39 | io.Copy(m, rOut) 40 | }() 41 | 42 | os.Stdout = wOut 43 | redirectStderr(wOut) 44 | 45 | logrus.SetLevel(logrus.DebugLevel) 46 | if isDebug { 47 | logrus.SetLevel(logrus.TraceLevel) 48 | } 49 | logrus.SetOutput(originalStdout) 50 | logrus.AddHook(lfshook.NewHook(logFile, &logrus.TextFormatter{ 51 | DisableColors: true, 52 | })) 53 | } 54 | 55 | type logTransport struct { 56 | rt http.RoundTripper 57 | } 58 | 59 | func (t *logTransport) RoundTrip(req *http.Request) (*http.Response, error) { 60 | logrus.Tracef("Request %s", req.URL.String()) 61 | return t.rt.RoundTrip(req) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/bedrocktool/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "runtime/pprof" 12 | "syscall" 13 | 14 | "github.com/bedrock-tool/bedrocktool/locale" 15 | "github.com/bedrock-tool/bedrocktool/ui" 16 | "github.com/bedrock-tool/bedrocktool/ui/cli" 17 | "github.com/bedrock-tool/bedrocktool/utils" 18 | "github.com/bedrock-tool/bedrocktool/utils/commands" 19 | "github.com/bedrock-tool/bedrocktool/utils/osabs" 20 | "github.com/bedrock-tool/bedrocktool/utils/xbox" 21 | 22 | _ "github.com/bedrock-tool/bedrocktool/subcommands" 23 | 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | var uis = map[string]ui.UI{} 28 | 29 | func selectUI() ui.UI { 30 | if len(os.Args) == 1 { 31 | if ui, ok := uis["gui"]; ok { 32 | return ui 33 | } else { 34 | c := uis["cli"].(*cli.CLI) 35 | c.IsInteractive = true 36 | } 37 | } 38 | return uis["cli"] 39 | } 40 | 41 | func main() { 42 | if err := osabs.Init(); err != nil { 43 | panic(err) 44 | } 45 | baseDir := osabs.GetDataDir() 46 | if baseDir != "" { 47 | if err := os.Chdir(baseDir); err != nil { 48 | panic(err) 49 | } 50 | } 51 | 52 | setupLogging(utils.IsDebug()) 53 | log := logrus.WithField("part", "main") 54 | ctx, cancel := context.WithCancelCause(context.Background()) 55 | 56 | utils.ErrorHandler = func(err error) { 57 | if utils.IsDebug() { 58 | panic(err) 59 | } 60 | utils.PrintPanic(err) 61 | cancel(err) 62 | 63 | var IsInteractive bool 64 | if c, ok := uis["cli"].(*cli.CLI); ok { 65 | IsInteractive = c.IsInteractive 66 | } 67 | if IsInteractive { 68 | input := bufio.NewScanner(os.Stdin) 69 | input.Scan() 70 | } 71 | os.Exit(1) 72 | } 73 | 74 | if utils.IsDebug() { 75 | f, err := os.Create("cpu.pprof") 76 | if err != nil { 77 | panic(err) 78 | } 79 | pprof.StartCPUProfile(f) 80 | defer pprof.StopCPUProfile() 81 | 82 | defer func() { 83 | f, err := os.Create("mem.pprof") 84 | if err != nil { 85 | panic(err) 86 | } 87 | defer f.Close() 88 | pprof.WriteHeapProfile(f) 89 | }() 90 | 91 | http.DefaultTransport = &logTransport{rt: http.DefaultTransport} 92 | } else { 93 | log.Info(locale.Loc("bedrocktool_version", locale.Strmap{"Version": utils.Version})) 94 | } 95 | 96 | env, ok := os.LookupEnv("BEDROCK_ENV") 97 | if !ok { 98 | env = "prod" 99 | } 100 | if err := utils.Auth.Startup(env); err != nil { 101 | logrus.Fatal(err) 102 | } 103 | 104 | // exit cleanup 105 | sigs := make(chan os.Signal, 1) 106 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 107 | go func() { 108 | <-sigs 109 | cancel(errors.New("program closing")) 110 | }() 111 | 112 | ui := selectUI() 113 | if err := ui.Init(); err != nil { 114 | log.Errorf("Failed to init UI %s", err) 115 | return 116 | } 117 | err := ui.Start(ctx, cancel) 118 | if err != nil && !errors.Is(err, context.Canceled) { 119 | log.Error(err) 120 | } 121 | } 122 | 123 | type TransSettings struct { 124 | Auth bool `opt:"Auth" flag:"auth" desc:"locale.should_login_xbox"` 125 | } 126 | 127 | type TransCMD struct{} 128 | 129 | func (TransCMD) Name() string { 130 | return "trans" 131 | } 132 | 133 | func (TransCMD) Description() string { 134 | return "" 135 | } 136 | 137 | func (TransCMD) Settings() any { 138 | return new(TransSettings) 139 | } 140 | 141 | func (TransCMD) Run(ctx context.Context, settings any) error { 142 | transSettings := settings.(*TransSettings) 143 | const ( 144 | BlackFg = "\033[30m" 145 | Bold = "\033[1m" 146 | Blue = "\033[46m" 147 | Pink = "\033[45m" 148 | White = "\033[47m" 149 | Reset = "\033[0m" 150 | ) 151 | if transSettings.Auth { 152 | if utils.Auth.LoggedIn() { 153 | logrus.Info("Already Logged in") 154 | } else { 155 | utils.Auth.Login(ctx, &xbox.DeviceTypeAndroid) 156 | } 157 | } 158 | fmt.Println(BlackFg + Bold + Blue + " Trans " + Pink + " Rights " + White + " Are " + Pink + " Human " + Blue + " Rights " + Reset) 159 | return nil 160 | } 161 | 162 | func init() { 163 | commands.RegisterCommand(&TransCMD{}) 164 | } 165 | -------------------------------------------------------------------------------- /cmd/dump-actors/usage.md: -------------------------------------------------------------------------------- 1 | go run ./cmd/dump-actors [world-folder] 2 | this makes actors.json, you can then edit that file if you want 3 | 4 | go run ./cmd/dump-actors [world-folder] [output-folder] 5 | this copies the world, applies the entities from actors.json to it -------------------------------------------------------------------------------- /cmd/generate-color-lookup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sort" 8 | 9 | "github.com/bedrock-tool/bedrocktool/utils" 10 | "github.com/sandertv/gophertunnel/minecraft/resource" 11 | "golang.org/x/exp/maps" 12 | ) 13 | 14 | func main() { 15 | if len(os.Args) != 2 { 16 | println("usage: generate-color-lookup ") 17 | } 18 | folder := os.Args[1] 19 | packNames, err := os.ReadDir(folder) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | var packs []resource.Pack 25 | for _, fi := range packNames { 26 | name := fi.Name() 27 | pack, err := utils.PackFromBase(resource.MustReadPath(folder + "/" + name)) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | packs = append(packs, pack) 32 | } 33 | 34 | colors := utils.ResolveColors(nil, packs) 35 | keys := maps.Keys(colors) 36 | sort.Strings(keys) 37 | 38 | f, err := os.Create("blockcolors.go") 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | defer f.Close() 43 | 44 | f.WriteString("package utils\n\n") 45 | f.WriteString("import (\n\t\"image/color\"\n)\n\n") 46 | 47 | f.WriteString("func LookupColor(name string) color.RGBA {\n") 48 | f.WriteString("\tswitch name {\n") 49 | for _, name := range keys { 50 | color := colors[name] 51 | f.WriteString("\tcase \"" + name + "\":\n") 52 | f.WriteString(fmt.Sprintf("\t\treturn color.RGBA{0x%x, 0x%x, 0x%x, 0x%x}\n", color.R, color.G, color.B, color.A)) 53 | } 54 | f.WriteString("\tdefault:\n\t\treturn color.RGBA{0xff, 0x00, 0xff, 0x00}\n") 55 | f.WriteString("\t}\n") 56 | f.WriteString("}\n") 57 | } 58 | -------------------------------------------------------------------------------- /cmd/libbedrocktool/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "unsafe" 6 | 7 | "github.com/bedrock-tool/bedrocktool/ui/rui" 8 | "github.com/bedrock-tool/bedrocktool/utils" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | /* 13 | #include 14 | typedef void (*bedrocktoolHandler)(uint8_t* data, size_t length); 15 | void call_bedrocktool_handler(bedrocktoolHandler h, uint8_t* data, size_t length) ; 16 | */ 17 | import "C" 18 | 19 | var ui = &rui.Rui{} 20 | 21 | func main() { 22 | select {} 23 | } 24 | 25 | //export BedrocktoolInit 26 | func BedrocktoolInit() int { 27 | env, ok := os.LookupEnv("BEDROCK_ENV") 28 | if !ok { 29 | env = "prod" 30 | } 31 | err := utils.Auth.Startup(env) 32 | if err != nil { 33 | logrus.Error(err) 34 | return -1 35 | } 36 | 37 | if err := ui.Init(); err != nil { 38 | logrus.Error(err) 39 | return -1 40 | } 41 | 42 | return 0 43 | } 44 | 45 | //export BedrocktoolSetHandler 46 | func BedrocktoolSetHandler(handler C.bedrocktoolHandler) { 47 | ui.HandlerLoop(func(b []byte) { 48 | C.call_bedrocktool_handler(handler, (*C.uint8_t)(&b[0]), C.size_t(len(b))) 49 | }) 50 | } 51 | 52 | //export BedrocktoolSendPacket 53 | func BedrocktoolSendPacket(dataP *byte, length C.size_t) { 54 | data := unsafe.Slice(dataP, int(length)) 55 | ui.ReceiveUIPacket(data) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/libbedrocktool/msg_cb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include 5 | typedef void (*bedrocktoolHandler)(uint8_t* data, size_t length); 6 | void call_bedrocktool_handler(bedrocktoolHandler h, uint8_t* data, size_t length) { 7 | h(data, length); 8 | } 9 | */ 10 | import "C" 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bedrock-tool/bedrocktool 2 | 3 | go 1.24.0 4 | 5 | // that repo has a bad go.mod so need to force the old version 6 | replace github.com/brentp/intintmap => github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 7 | 8 | require ( 9 | gioui.org v0.8.0 10 | gioui.org/x v0.8.1 11 | git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 12 | github.com/OneOfOne/xxhash v1.2.8 13 | github.com/chzyer/readline v1.5.1 14 | github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 15 | github.com/dblezek/tga v0.0.0-20150626111426-80720cbc1017 16 | github.com/denisbrodbeck/machineid v1.0.1 17 | github.com/df-mc/dragonfly v0.10.3 18 | github.com/df-mc/goleveldb v1.1.9 19 | github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c 20 | github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 21 | github.com/fatih/color v1.18.0 22 | github.com/gioui-plugins/gio-plugins v0.1.1-0.20241219101404-5bad9f318498 23 | github.com/go-gl/mathgl v1.2.0 24 | github.com/go-jose/go-jose/v3 v3.0.4 25 | github.com/google/uuid v1.6.0 26 | github.com/klauspost/compress v1.18.0 27 | github.com/minio/selfupdate v0.6.0 28 | github.com/nicksnyder/go-i18n/v2 v2.6.0 29 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 30 | github.com/sandertv/go-raknet v1.14.3-0.20250305181847-6af3e95113d6 31 | github.com/sandertv/gophertunnel v1.45.1 32 | github.com/shirou/gopsutil/v3 v3.24.5 33 | github.com/sirupsen/logrus v1.9.3 34 | github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33 35 | github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e 36 | golang.design/x/lockfree v0.0.1 37 | golang.org/x/crypto v0.37.0 38 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 39 | golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 40 | golang.org/x/oauth2 v0.29.0 41 | golang.org/x/sys v0.32.0 42 | golang.org/x/text v0.24.0 43 | gopkg.in/yaml.v3 v3.0.1 44 | ) 45 | 46 | require ( 47 | aead.dev/minisign v0.3.0 // indirect 48 | gioui.org/shader v1.0.8 // indirect 49 | github.com/brentp/intintmap v0.0.0-20230108034600-4d14af6efe11 // indirect 50 | github.com/changkun/lockfree v0.0.1 // indirect 51 | github.com/df-mc/worldupgrader v1.0.20-0.20250218221316-e2a2610f4655 // indirect 52 | github.com/dlclark/regexp2 v1.11.5 // indirect 53 | github.com/ftrvxmtrx/tga v0.0.0-20150524081124-bd8e8d5be13a // indirect 54 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 55 | github.com/go-ole/go-ole v1.3.0 // indirect 56 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 57 | github.com/go-text/typesetting v0.3.0 // indirect 58 | github.com/godbus/dbus/v5 v5.0.6 // indirect 59 | github.com/golang/snappy v1.0.0 // indirect 60 | github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect 61 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 62 | github.com/mattn/go-colorable v0.1.14 // indirect 63 | github.com/mattn/go-isatty v0.0.20 // indirect 64 | github.com/muhammadmuzzammil1998/jsonc v1.0.0 // indirect 65 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 66 | github.com/segmentio/fasthash v1.0.3 // indirect 67 | github.com/tklauser/go-sysconf v0.3.15 // indirect 68 | github.com/tklauser/numcpus v0.10.0 // indirect 69 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 70 | golang.org/x/image v0.26.0 // indirect 71 | golang.org/x/net v0.39.0 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.0 2 | 3 | use ( 4 | . 5 | ./dragonfly 6 | ./go-raknet 7 | ./gophertunnel 8 | ) 9 | -------------------------------------------------------------------------------- /handlers/chat_log.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "time" 8 | 9 | "github.com/bedrock-tool/bedrocktool/utils/proxy" 10 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type chatLogger struct { 15 | Verbose bool 16 | fio *os.File 17 | } 18 | 19 | func (c *chatLogger) PacketCB(session *proxy.Session, pk packet.Packet, toServer bool, t time.Time, _ bool) (packet.Packet, error) { 20 | if text, ok := pk.(*packet.Text); ok { 21 | logLine := text.Message 22 | if c.Verbose { 23 | logLine += fmt.Sprintf(" (TextType: %d | XUID: %s | PlatformChatID: %s)", text.TextType, text.XUID, text.PlatformChatID) 24 | } 25 | c.fio.WriteString(fmt.Sprintf("[%s] ", t.Format(time.RFC3339))) 26 | logrus.Info(logLine) 27 | if toServer { 28 | c.fio.WriteString("SENT: ") 29 | } 30 | c.fio.WriteString(logLine + "\n") 31 | } 32 | return pk, nil 33 | } 34 | 35 | func NewChatLogger() func() *proxy.Handler { 36 | return func() *proxy.Handler { 37 | c := &chatLogger{} 38 | return &proxy.Handler{ 39 | Name: "Packet Capturer", 40 | PacketCallback: c.PacketCB, 41 | SessionStart: func(s *proxy.Session, serverName string) error { 42 | filename := fmt.Sprintf("%s_%s_chat.log", serverName, time.Now().Format("2006-01-02_15-04-05_Z07")) 43 | f, err := os.Create(filename) 44 | if err != nil { 45 | return err 46 | } 47 | c.fio = f 48 | return nil 49 | }, 50 | OnSessionEnd: func(_ *proxy.Session, _ *sync.WaitGroup) { 51 | c.fio.Close() 52 | }, 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /handlers/worlds/biome.go: -------------------------------------------------------------------------------- 1 | package worlds 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/sandertv/gophertunnel/minecraft/protocol" 7 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 8 | ) 9 | 10 | type customBiome struct { 11 | name string 12 | biome protocol.BiomeDefinition 13 | pk *packet.BiomeDefinitionList 14 | } 15 | 16 | func (c *customBiome) Temperature() float64 { 17 | return 0.5 18 | } 19 | 20 | func (c *customBiome) Rainfall() float64 { 21 | return 0.5 22 | } 23 | 24 | func (c *customBiome) Depth() float64 { 25 | return 0 26 | } 27 | 28 | func (c *customBiome) Scale() float64 { 29 | return 0 30 | } 31 | 32 | func (c *customBiome) WaterColour() color.RGBA { 33 | return color.RGBA{} 34 | } 35 | 36 | func (c *customBiome) Tags() []string { 37 | return nil 38 | } 39 | 40 | func (c *customBiome) String() string { 41 | return c.name 42 | } 43 | 44 | func (c *customBiome) EncodeBiome() int { 45 | return 0 46 | } 47 | -------------------------------------------------------------------------------- /handlers/worlds/scripting/events.go: -------------------------------------------------------------------------------- 1 | package scripting 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "time" 7 | 8 | "github.com/bedrock-tool/bedrocktool/handlers/worlds/entity" 9 | "github.com/bedrock-tool/bedrocktool/utils" 10 | "github.com/df-mc/dragonfly/server/world" 11 | "github.com/dop251/goja" 12 | "github.com/go-gl/mathgl/mgl32" 13 | "github.com/sandertv/gophertunnel/minecraft/protocol" 14 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 15 | ) 16 | 17 | func (v *VM) OnEntityAdd(entity *entity.Entity, timeReceived time.Time) (apply bool) { 18 | if v.CB.OnEntityAdd == nil { 19 | return true 20 | } 21 | apply = true 22 | err := utils.RecoverCall(func() error { 23 | applyV := v.CB.OnEntityAdd(entity, newEntityDataObject(v.runtime, entity.Metadata), float64(timeReceived.UnixMilli())) 24 | if !goja.IsUndefined(applyV) { 25 | apply = applyV.ToBoolean() 26 | } 27 | return nil 28 | }) 29 | if err != nil { 30 | v.log.Error(err) 31 | } 32 | return 33 | } 34 | 35 | func (v *VM) OnEntityDataUpdate(entity *entity.Entity, timeReceived time.Time) { 36 | if v.CB.OnEntityDataUpdate == nil { 37 | return 38 | } 39 | err := utils.RecoverCall(func() error { 40 | v.CB.OnEntityDataUpdate(entity, newEntityDataObject(v.runtime, entity.Metadata), float64(timeReceived.UnixMilli())) 41 | return nil 42 | }) 43 | if err != nil { 44 | v.log.Error(err) 45 | } 46 | } 47 | 48 | func (v *VM) OnChunkAdd(pos world.ChunkPos, timeReceived time.Time) (apply bool) { 49 | if v.CB.OnChunkAdd == nil { 50 | return true 51 | } 52 | err := utils.RecoverCall(func() error { 53 | applyV := v.CB.OnChunkAdd(pos, float64(timeReceived.UnixMilli())) 54 | if !goja.IsUndefined(applyV) { 55 | apply = applyV.ToBoolean() 56 | } 57 | return nil 58 | }) 59 | if err != nil { 60 | v.log.Error(err) 61 | apply = true 62 | } 63 | return 64 | } 65 | 66 | func (v *VM) OnBlockUpdate(name string, properties map[string]any, pos protocol.BlockPos, timeReceived time.Time) (apply bool) { 67 | if v.CB.OnBlockUpdate == nil { 68 | return true 69 | } 70 | 71 | apply = true 72 | err := utils.RecoverCall(func() error { 73 | applyV := v.CB.OnBlockUpdate(name, properties, pos, float64(timeReceived.UnixMilli())) 74 | if !goja.IsUndefined(applyV) { 75 | apply = applyV.ToBoolean() 76 | } 77 | return nil 78 | }) 79 | if err != nil { 80 | v.log.Error(err) 81 | return true 82 | } 83 | 84 | return apply 85 | } 86 | 87 | func (v *VM) OnSpawnParticle(name string, position mgl32.Vec3, timeReceived time.Time) { 88 | if v.CB.OnSpawnParticle == nil { 89 | return 90 | } 91 | 92 | err := utils.RecoverCall(func() error { 93 | v.CB.OnSpawnParticle(name, position, float64(timeReceived.UnixMilli())) 94 | return nil 95 | }) 96 | if err != nil { 97 | v.log.Error(err) 98 | } 99 | } 100 | 101 | func (v *VM) OnPacket(pk packet.Packet, toServer bool, timeReceived time.Time) (drop bool) { 102 | if v.CB.OnPacket == nil { 103 | return false 104 | } 105 | 106 | v.lock.Lock() 107 | defer v.lock.Unlock() 108 | err := utils.RecoverCall(func() error { 109 | packetName := strings.Split(reflect.TypeOf(pk).String(), ".")[1] 110 | drop = v.CB.OnPacket(packetName, pk, toServer, float64(timeReceived.UnixMilli())) 111 | return nil 112 | }) 113 | if err != nil { 114 | v.log.Error(err) 115 | return false 116 | } 117 | return drop 118 | } 119 | -------------------------------------------------------------------------------- /handlers/worlds/scripting/scripting.go: -------------------------------------------------------------------------------- 1 | package scripting 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/bedrock-tool/bedrocktool/handlers/worlds/entity" 9 | "github.com/bedrock-tool/bedrocktool/utils" 10 | "github.com/df-mc/dragonfly/server/world" 11 | "github.com/dop251/goja" 12 | "github.com/dop251/goja_nodejs/console" 13 | "github.com/dop251/goja_nodejs/require" 14 | "github.com/go-gl/mathgl/mgl32" 15 | "github.com/sandertv/gophertunnel/minecraft/protocol" 16 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 17 | "github.com/sirupsen/logrus" 18 | 19 | _ "embed" 20 | ) 21 | 22 | type VM struct { 23 | runtime *goja.Runtime 24 | lock sync.Mutex 25 | log *logrus.Entry 26 | 27 | CB struct { 28 | OnEntityAdd func(entity *entity.Entity, metadata *goja.Object, timeReceived float64) (apply goja.Value) 29 | OnChunkAdd func(pos world.ChunkPos, timeReceived float64) (apply goja.Value) 30 | OnEntityDataUpdate func(entity *entity.Entity, metadata *goja.Object, timeReceived float64) 31 | OnBlockUpdate func(name string, properties map[string]any, pos protocol.BlockPos, timeReceived float64) (apply goja.Value) 32 | OnSpawnParticle func(name string, pos mgl32.Vec3, timeReceived float64) 33 | OnPacket func(name string, pk packet.Packet, toServer bool, timeReceived float64) (drop bool) 34 | } 35 | } 36 | 37 | func New() *VM { 38 | v := &VM{ 39 | runtime: goja.New(), 40 | log: logrus.WithField("part", "jsvm"), 41 | } 42 | 43 | registry := new(require.Registry) 44 | registry.Enable(v.runtime) 45 | console.Enable(v.runtime) 46 | 47 | events := v.runtime.NewObject() 48 | events.Set("register", func(name string, callback goja.Value) (err error) { 49 | switch name { 50 | case "EntityAdd": 51 | err = v.runtime.ExportTo(callback, &v.CB.OnEntityAdd) 52 | case "EntityDataUpdate": 53 | err = v.runtime.ExportTo(callback, &v.CB.OnEntityDataUpdate) 54 | case "ChunkAdd": 55 | err = v.runtime.ExportTo(callback, &v.CB.OnChunkAdd) 56 | case "BlockUpdate": 57 | err = v.runtime.ExportTo(callback, &v.CB.OnBlockUpdate) 58 | case "SpawnParticle": 59 | err = v.runtime.ExportTo(callback, &v.CB.OnSpawnParticle) 60 | case "Packet": 61 | err = v.runtime.ExportTo(callback, &v.CB.OnPacket) 62 | } 63 | return err 64 | }) 65 | v.runtime.GlobalObject().Set("events", events) 66 | 67 | fs := v.runtime.NewObject() 68 | fs.Set("create", func(call goja.FunctionCall) goja.Value { 69 | name := call.Argument(0).String() 70 | file, err := os.Create(utils.PathData(name)) 71 | if err != nil { 72 | return v.runtime.ToValue(fmt.Errorf("failed to create file '%s': %w", name, err)) 73 | } 74 | 75 | obj := v.runtime.NewObject() 76 | obj.Set("write", func(call goja.FunctionCall) goja.Value { 77 | data := call.Argument(0).String() 78 | _, err := file.WriteString(data) 79 | if err != nil { 80 | return v.runtime.ToValue(fmt.Errorf("failed to write to file '%s': %w", name, err)) 81 | } 82 | return goja.Undefined() 83 | }) 84 | obj.Set("close", func(call goja.FunctionCall) goja.Value { 85 | err := file.Close() 86 | if err != nil { 87 | return v.runtime.ToValue(fmt.Errorf("failed to close file '%s': %w", name, err)) 88 | } 89 | return goja.Undefined() 90 | }) 91 | 92 | return obj 93 | }) 94 | 95 | v.runtime.GlobalObject().Set("fs", fs) 96 | 97 | return v 98 | } 99 | 100 | func (v *VM) Load(script string) error { 101 | _, err := v.runtime.RunScript("script.js", script) 102 | if err != nil { 103 | return err 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /handlers/worlds/worldstate/packs.go: -------------------------------------------------------------------------------- 1 | package worldstate 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path" 7 | "slices" 8 | 9 | "github.com/bedrock-tool/bedrocktool/locale" 10 | "github.com/bedrock-tool/bedrocktool/ui/messages" 11 | "github.com/bedrock-tool/bedrocktool/utils" 12 | "github.com/sandertv/gophertunnel/minecraft/resource" 13 | "github.com/sandertv/gophertunnel/minecraft/text" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | func addPacksJSON(fs utils.WriterFS, name string, deps []resourcePackDependency) error { 18 | f, err := fs.Create(name) 19 | if err != nil { 20 | return err 21 | } 22 | defer f.Close() 23 | if err := json.NewEncoder(f).Encode(deps); err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func (w *World) addResourcePacks() error { 30 | packNames := make(map[string][]string) 31 | for _, pack := range w.ResourcePacks { 32 | packName := utils.FormatPackName(pack.Name()) 33 | packNames[packName] = append(packNames[packName], pack.UUID().String()) 34 | } 35 | 36 | for _, pack := range w.ResourcePacks { 37 | log := w.log.WithField("pack", pack.Name()) 38 | if pack.Encrypted() && !pack.CanRead() { 39 | log.Warn("Cant add is encrypted") 40 | continue 41 | } 42 | logrus.Info(locale.Loc("adding_pack", locale.Strmap{"Name": text.Clean(pack.Name())})) 43 | 44 | messages.SendEvent(&messages.EventProcessingWorldUpdate{ 45 | WorldName: w.Name, 46 | State: "Adding Resourcepack " + text.Clean(pack.Name()), 47 | }) 48 | 49 | packName := utils.FormatPackName(pack.Name()) 50 | if packIds := packNames[packName]; len(packIds) > 1 { 51 | packName = fmt.Sprintf("%s_%d", packName[:8], slices.Index(packIds, pack.UUID().String())) 52 | } 53 | 54 | err := utils.CopyFS(pack, utils.SubFS(utils.OSWriter{Base: w.Folder}, path.Join("resource_packs", packName))) 55 | if err != nil { 56 | log.Error(err) 57 | continue 58 | } 59 | } 60 | 61 | messages.SendEvent(&messages.EventProcessingWorldUpdate{ 62 | WorldName: w.Name, 63 | State: "", 64 | }) 65 | 66 | return nil 67 | } 68 | 69 | type addedPack struct { 70 | BehaviorPack bool 71 | Header *resource.Header 72 | } 73 | 74 | func (w *World) finalizePacks(addAdditionalPacks func(fs utils.WriterFS) ([]addedPack, error)) error { 75 | err := <-w.resourcePacksDone 76 | if err != nil { 77 | return err 78 | } 79 | 80 | messages.SendEvent(&messages.EventProcessingWorldUpdate{ 81 | WorldName: w.Name, 82 | State: "Adding Behaviorpack", 83 | }) 84 | 85 | fs := utils.OSWriter{Base: w.Folder} 86 | additionalPacks, err := addAdditionalPacks(fs) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | var resourcePackDependencies []resourcePackDependency 92 | for _, pack := range w.ResourcePacks { 93 | resourcePackDependencies = append(resourcePackDependencies, resourcePackDependency{ 94 | UUID: pack.Manifest().Header.UUID.String(), 95 | Version: pack.Manifest().Header.Version, 96 | }) 97 | } 98 | 99 | var behaviorPackDependencies []resourcePackDependency 100 | for _, p := range additionalPacks { 101 | dep := resourcePackDependency{ 102 | UUID: p.Header.UUID.String(), 103 | Version: p.Header.Version, 104 | } 105 | if p.BehaviorPack { 106 | behaviorPackDependencies = append(behaviorPackDependencies, dep) 107 | } else { 108 | resourcePackDependencies = append(resourcePackDependencies, dep) 109 | } 110 | } 111 | 112 | if len(behaviorPackDependencies) > 0 { 113 | err := addPacksJSON(fs, "world_behavior_packs.json", behaviorPackDependencies) 114 | if err != nil { 115 | return err 116 | } 117 | } 118 | 119 | if len(resourcePackDependencies) > 0 { 120 | err := addPacksJSON(fs, "world_resource_packs.json", resourcePackDependencies) 121 | if err != nil { 122 | return err 123 | } 124 | } 125 | 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /handlers/worlds/worldstate/players.go: -------------------------------------------------------------------------------- 1 | package worldstate 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bedrock-tool/bedrocktool/handlers/worlds/entity" 7 | "github.com/bedrock-tool/bedrocktool/utils/resourcepack" 8 | "github.com/go-gl/mathgl/mgl32" 9 | "github.com/sandertv/gophertunnel/minecraft/protocol" 10 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 11 | ) 12 | 13 | type player struct { 14 | add *packet.AddPlayer 15 | Position mgl32.Vec3 16 | Pitch, Yaw, HeadYaw float32 17 | } 18 | 19 | func (w *World) AddPlayer(pk *packet.AddPlayer) { 20 | w.players[pk.UUID] = &player{ 21 | add: pk, 22 | Position: pk.Position, 23 | Pitch: pk.Pitch, 24 | Yaw: pk.Yaw, 25 | HeadYaw: pk.HeadYaw, 26 | } 27 | } 28 | 29 | func (w *World) playersToEntities() (out []resourcepack.EntityPlayer) { 30 | for _, p := range w.players { 31 | metadata := protocol.NewEntityMetadata() 32 | metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) 33 | metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) 34 | metadata[protocol.EntityDataKeyName] = p.add.Username 35 | identifier := fmt.Sprintf("bedrocktool_player:%s", p.add.UUID) 36 | w.currState().StoreEntity(p.add.EntityRuntimeID, &entity.Entity{ 37 | RuntimeID: p.add.EntityRuntimeID, 38 | UniqueID: int64(p.add.EntityRuntimeID), 39 | EntityType: identifier, 40 | Position: p.Position, 41 | Pitch: p.Pitch, 42 | Yaw: p.Yaw, 43 | HeadYaw: p.HeadYaw, 44 | Metadata: metadata, 45 | }) 46 | out = append(out, resourcepack.EntityPlayer{ 47 | Identifier: identifier, 48 | UUID: p.add.UUID, 49 | }) 50 | } 51 | return out 52 | } 53 | -------------------------------------------------------------------------------- /handlers/worlds/worldstate/worldstate.go: -------------------------------------------------------------------------------- 1 | package worldstate 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | 7 | "github.com/bedrock-tool/bedrocktool/handlers/worlds/entity" 8 | "github.com/bedrock-tool/bedrocktool/utils" 9 | "github.com/df-mc/dragonfly/server/block/cube" 10 | "github.com/df-mc/dragonfly/server/world" 11 | "github.com/df-mc/dragonfly/server/world/chunk" 12 | "github.com/sandertv/gophertunnel/minecraft/protocol" 13 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 14 | "github.com/thomaso-mirodin/intmath/i32" 15 | ) 16 | 17 | type memoryState struct { 18 | maps map[int64]*Map 19 | chunks map[world.ChunkPos]*Chunk 20 | entities map[entity.RuntimeID]*entity.Entity 21 | entityLinks map[entity.UniqueID]map[entity.UniqueID]struct{} 22 | 23 | uniqueIDsToRuntimeIDs map[entity.UniqueID]entity.RuntimeID 24 | } 25 | 26 | type Chunk struct { 27 | *chunk.Chunk 28 | BlockEntities map[cube.Pos]map[string]any 29 | } 30 | 31 | func newWorldState() *memoryState { 32 | return &memoryState{ 33 | maps: make(map[int64]*Map), 34 | chunks: make(map[world.ChunkPos]*Chunk), 35 | entities: make(map[entity.RuntimeID]*entity.Entity), 36 | entityLinks: make(map[entity.UniqueID]map[entity.UniqueID]struct{}), 37 | 38 | uniqueIDsToRuntimeIDs: make(map[entity.UniqueID]entity.RuntimeID), 39 | } 40 | } 41 | 42 | func (w *memoryState) StoreChunk(pos world.ChunkPos, ch *Chunk) { 43 | w.chunks[pos] = ch 44 | } 45 | 46 | func (w *memoryState) StoreMap(m *packet.ClientBoundMapItemData) { 47 | return // not finished yet 48 | m1, ok := w.maps[m.MapID] 49 | if !ok { 50 | m1 = &Map{ 51 | MapID: m.MapID, 52 | Height: 128, 53 | Width: 128, 54 | Scale: 1, 55 | Dimension: 0, 56 | ZCenter: m.Origin.Z(), 57 | XCenter: m.Origin.X(), 58 | } 59 | w.maps[m.MapID] = m1 60 | } 61 | draw.Draw(&image.RGBA{ 62 | Pix: m1.Colors[:], 63 | Rect: image.Rect(0, 0, int(m.Width), int(m.Height)), 64 | Stride: int(m.Width) * 4, 65 | }, image.Rect( 66 | int(m.XOffset), int(m.YOffset), 67 | int(m.Width), int(m.Height), 68 | ), utils.RGBA2Img(m.Pixels, int(m.Width), int(m.Height)), 69 | image.Point{}, 70 | draw.Over, 71 | ) 72 | } 73 | 74 | func (w *memoryState) cullChunks() { 75 | chunks: 76 | for key, ch := range w.chunks { 77 | for _, sub := range ch.Sub() { 78 | if !sub.Empty() { 79 | continue chunks 80 | } 81 | } 82 | delete(w.chunks, key) 83 | } 84 | } 85 | 86 | func (w *memoryState) ApplyTo(w2 worldStateInterface, around cube.Pos, radius int32, cf func(world.ChunkPos, *chunk.Chunk)) { 87 | w.cullChunks() 88 | for pos, ch := range w.chunks { 89 | dist := i32.Sqrt(i32.Pow(pos.X()-int32(around.X()/16), 2) + i32.Pow(pos.Z()-int32(around.Z()/16), 2)) 90 | if dist <= radius || radius < 0 { 91 | w2.StoreChunk(pos, ch) 92 | cf(pos, ch.Chunk) 93 | } else { 94 | cf(pos, nil) 95 | } 96 | } 97 | 98 | for k, es := range w.entities { 99 | x := int(es.Position[0]) 100 | z := int(es.Position[2]) 101 | dist := i32.Sqrt(i32.Pow(int32(x-around.X()), 2) + i32.Pow(int32(z-around.Z()), 2)) 102 | e2 := w2.GetEntity(k) 103 | if e2 != nil || dist < radius*16 || radius < 0 { 104 | w2.StoreEntity(k, es) 105 | } 106 | } 107 | } 108 | 109 | func cubePosInChunk(pos cube.Pos) (p world.ChunkPos, sp int16) { 110 | p[0] = int32(pos.X() >> 4) 111 | sp = int16(pos.Y() >> 4) 112 | p[1] = int32(pos.Z() >> 4) 113 | return 114 | } 115 | 116 | func (w *memoryState) StoreEntity(id entity.RuntimeID, es *entity.Entity) { 117 | w.entities[id] = es 118 | w.uniqueIDsToRuntimeIDs[es.UniqueID] = es.RuntimeID 119 | } 120 | 121 | func (w *memoryState) GetEntity(id entity.RuntimeID) *entity.Entity { 122 | return w.entities[id] 123 | } 124 | 125 | func (w *memoryState) AddEntityLink(el protocol.EntityLink) { 126 | switch el.Type { 127 | case protocol.EntityLinkPassenger: 128 | fallthrough 129 | case protocol.EntityLinkRider: 130 | if _, ok := w.entityLinks[el.RiddenEntityUniqueID]; !ok { 131 | w.entityLinks[el.RiddenEntityUniqueID] = make(map[int64]struct{}) 132 | } 133 | w.entityLinks[el.RiddenEntityUniqueID][el.RiderEntityUniqueID] = struct{}{} 134 | case protocol.EntityLinkRemove: 135 | delete(w.entityLinks[el.RiddenEntityUniqueID], el.RiderEntityUniqueID) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bedrock-tool/bedrocktool/ff59f55cfa0cd02a4825976dd1b5e13c679b8a9a/icon.png -------------------------------------------------------------------------------- /locale/i18n.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | 7 | "github.com/nicksnyder/go-i18n/v2/i18n" 8 | "github.com/sirupsen/logrus" 9 | "golang.org/x/text/language" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type Strmap map[string]interface{} 14 | 15 | //go:embed *.yaml 16 | var localesFS embed.FS 17 | var lang *i18n.Localizer 18 | 19 | func load_language(bundle *i18n.Bundle, tag language.Tag) error { 20 | _, err := bundle.LoadMessageFileFS(localesFS, fmt.Sprintf("%s.yaml", tag.String())) 21 | return err 22 | } 23 | 24 | func init() { 25 | var defaultTag language.Tag = language.English 26 | var err error 27 | 28 | // get default language 29 | languageName := getLanguageName() 30 | defaultTag, err = language.Parse(languageName) 31 | if err != nil { 32 | logrus.Warn("failed to parse language name") 33 | } 34 | 35 | bundle := i18n.NewBundle(defaultTag) 36 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 37 | 38 | err = load_language(bundle, defaultTag) 39 | if err != nil { 40 | //logrus.Warnf("Couldnt load Language %s", languageName) 41 | err = load_language(bundle, language.English) 42 | if err != nil { 43 | logrus.Error("failed to load english language") 44 | } 45 | } 46 | 47 | lang = i18n.NewLocalizer(bundle, "en") 48 | } 49 | 50 | func Loc(id string, tmpl Strmap) string { 51 | s, err := lang.Localize(&i18n.LocalizeConfig{ 52 | MessageID: id, 53 | TemplateData: tmpl, 54 | }) 55 | if err != nil { 56 | return fmt.Sprintf("failed to translate! %s", id) 57 | } 58 | return s 59 | } 60 | 61 | func Locm(id string, tmpl Strmap, count int) string { 62 | s, err := lang.Localize(&i18n.LocalizeConfig{ 63 | MessageID: id, 64 | TemplateData: tmpl, 65 | PluralCount: count, 66 | }) 67 | if err != nil { 68 | return fmt.Sprintf("failed to translate! %s", id) 69 | } 70 | return s 71 | } 72 | -------------------------------------------------------------------------------- /locale/lang.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | 3 | package locale 4 | 5 | import "github.com/cloudfoundry/jibber_jabber" 6 | 7 | func getLanguageName() string { 8 | languageName, _ := jibber_jabber.DetectLanguage() 9 | return languageName 10 | } 11 | -------------------------------------------------------------------------------- /locale/lang_js.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | func getLanguageName() string { 4 | return "en" 5 | } 6 | -------------------------------------------------------------------------------- /scripting/example.js: -------------------------------------------------------------------------------- 1 | events.register('EntityAdd', (entity, metadata, time) => { 2 | console.log(`EntityAdd ${entity.EntityType}`); 3 | }); 4 | 5 | 6 | events.register('EntityDataUpdate', (entity, metadata, time) => { 7 | console.log(`EntityDataUpdate ${entity.EntityType}`); 8 | }); 9 | 10 | 11 | events.register('ChunkAdd', (pos, time) => { 12 | console.log(`ChunkAdd ${pos}`); 13 | }); 14 | 15 | 16 | events.register('BlockUpdate', (name, properties, pos, time) => { 17 | console.log(`BlockUpdate ${name}`); 18 | }); 19 | 20 | 21 | events.register('SpawnParticle', (name, pos, time) => { 22 | console.log(`SpawnParticle ${name}`); 23 | }); 24 | 25 | events.register('Packet', (name, packet, toServer, time) => { 26 | if(name === 'LevelSoundEvent') { 27 | console.log(`Packet ${name} ${JSON.stringify(packet)}`); 28 | } 29 | }); -------------------------------------------------------------------------------- /scripting/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "checkJs": true, 5 | "types": ["./bedrocktool"], 6 | "lib": ["ES5"] 7 | }, 8 | "include": ["**/*.js"] 9 | } -------------------------------------------------------------------------------- /scripting/scripting-ts/example.ts: -------------------------------------------------------------------------------- 1 | events.register('EntityAdd', (entity, metadata, properties, time) => { 2 | console.log(`EntityAdd ${entity.EntityType}`); 3 | }); 4 | 5 | 6 | events.register('EntityDataUpdate', (entity, metadata, properties, time) => { 7 | console.log(`EntityDataUpdate ${entity.EntityType}`); 8 | }); 9 | 10 | 11 | events.register('ChunkAdd', (pos, time) => { 12 | console.log(`ChunkAdd ${pos}`); 13 | }); 14 | 15 | 16 | events.register('BlockUpdate', (name, properties, pos, time) => { 17 | console.log(`BlockUpdate ${name}`); 18 | }); 19 | 20 | 21 | events.register('SpawnParticle', (name, pos, time) => { 22 | console.log(`SpawnParticle ${name}`); 23 | }); 24 | 25 | events.register('Packet', (name, packet, toServer, time) => { 26 | if(name === 'LevelSoundEvent') { 27 | console.log(`Packet ${name} ${JSON.stringify(packet)}`); 28 | } 29 | }); -------------------------------------------------------------------------------- /scripting/scripting-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "checkJs": true, 5 | "types": ["../bedrocktool"], 6 | "lib": ["ES5"] 7 | }, 8 | "include": ["**/*.ts"] 9 | } -------------------------------------------------------------------------------- /subcommands/capture.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bedrock-tool/bedrocktool/locale" 7 | "github.com/bedrock-tool/bedrocktool/utils/commands" 8 | "github.com/bedrock-tool/bedrocktool/utils/proxy" 9 | ) 10 | 11 | type CaptureSettings struct { 12 | ProxySettings proxy.ProxySettings 13 | } 14 | 15 | type CaptureCMD struct{} 16 | 17 | func (CaptureCMD) Name() string { 18 | return "capture" 19 | } 20 | 21 | func (CaptureCMD) Description() string { 22 | return locale.Loc("capture_synopsis", nil) 23 | } 24 | 25 | func (CaptureCMD) Settings() any { 26 | return new(CaptureSettings) 27 | } 28 | 29 | func (CaptureCMD) Run(ctx context.Context, settings any) error { 30 | captureSettings := settings.(*CaptureSettings) 31 | 32 | captureSettings.ProxySettings.Capture = true 33 | p, err := proxy.New(ctx, captureSettings.ProxySettings) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return p.Run(true) 39 | } 40 | 41 | func init() { 42 | commands.RegisterCommand(&CaptureCMD{}) 43 | } 44 | -------------------------------------------------------------------------------- /subcommands/chat_log.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bedrock-tool/bedrocktool/handlers" 7 | "github.com/bedrock-tool/bedrocktool/locale" 8 | "github.com/bedrock-tool/bedrocktool/utils/commands" 9 | "github.com/bedrock-tool/bedrocktool/utils/proxy" 10 | ) 11 | 12 | type ChatLogSettings struct { 13 | ProxySettings proxy.ProxySettings 14 | 15 | Verbose bool `opt:"Verbose" flag:"verbose"` 16 | } 17 | 18 | type ChatLogCMD struct { 19 | ServerAddress string 20 | Verbose bool 21 | EnableClientCache bool 22 | } 23 | 24 | func (ChatLogCMD) Name() string { 25 | return "chat-log" 26 | } 27 | 28 | func (ChatLogCMD) Description() string { 29 | return locale.Loc("chat_log_synopsis", nil) 30 | } 31 | 32 | func (ChatLogCMD) Settings() any { 33 | return new(ChatLogSettings) 34 | } 35 | 36 | func (ChatLogCMD) Run(ctx context.Context, settings any) error { 37 | chatLogSettings := settings.(*ChatLogSettings) 38 | proxyContext, err := proxy.New(ctx, chatLogSettings.ProxySettings) 39 | if err != nil { 40 | return err 41 | } 42 | proxyContext.AddHandler(handlers.NewChatLogger()) 43 | return proxyContext.Run(true) 44 | } 45 | 46 | func init() { 47 | commands.RegisterCommand(&ChatLogCMD{}) 48 | } 49 | -------------------------------------------------------------------------------- /subcommands/debug.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bedrock-tool/bedrocktool/locale" 7 | "github.com/bedrock-tool/bedrocktool/utils/commands" 8 | "github.com/bedrock-tool/bedrocktool/utils/proxy" 9 | ) 10 | 11 | type DebugProxySettings struct { 12 | ProxySettings proxy.ProxySettings 13 | } 14 | 15 | type DebugProxyCMD struct{} 16 | 17 | func (DebugProxyCMD) Name() string { 18 | return "debug-proxy" 19 | } 20 | 21 | func (DebugProxyCMD) Description() string { 22 | return locale.Loc("debug_proxy_synopsis", nil) 23 | } 24 | 25 | func (DebugProxyCMD) Settings() any { 26 | return new(DebugProxySettings) 27 | } 28 | 29 | func (DebugProxyCMD) Run(ctx context.Context, settings any) error { 30 | debugProxySettings := settings.(*DebugProxySettings) 31 | debugProxySettings.ProxySettings.Debug = true 32 | p, err := proxy.New(ctx, debugProxySettings.ProxySettings) 33 | if err != nil { 34 | return err 35 | } 36 | return p.Run(true) 37 | } 38 | 39 | func init() { 40 | commands.RegisterCommand(&DebugProxyCMD{}) 41 | } 42 | -------------------------------------------------------------------------------- /subcommands/merge.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "math" 8 | "math/rand/v2" 9 | "os" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/bedrock-tool/bedrocktool/utils" 14 | "github.com/bedrock-tool/bedrocktool/utils/commands" 15 | "github.com/bedrock-tool/bedrocktool/utils/merge" 16 | "github.com/df-mc/dragonfly/server/world" 17 | "github.com/df-mc/dragonfly/server/world/mcdb" 18 | "github.com/df-mc/goleveldb/leveldb/opt" 19 | ) 20 | 21 | type MergeSettings struct { 22 | Bounds bool `opt:"Show Bounds" flag:"bounds"` 23 | OutPath string `opt:"Out Path" flag:"out"` 24 | InputWorlds []string `opt:"Input Worlds" flag:"-args"` 25 | } 26 | 27 | type MergeCMD struct{} 28 | 29 | func (MergeCMD) Name() string { 30 | return "merge" 31 | } 32 | 33 | func (MergeCMD) Description() string { 34 | return "merge worlds" 35 | } 36 | 37 | func (MergeCMD) Settings() any { 38 | return new(MergeSettings) 39 | } 40 | 41 | type worldInstance struct { 42 | Name string 43 | db *mcdb.DB 44 | offset world.ChunkPos 45 | } 46 | 47 | func (c MergeCMD) Run(ctx context.Context, settings any) error { 48 | mergeSettings := settings.(*MergeSettings) 49 | if mergeSettings.OutPath == "" && !mergeSettings.Bounds { 50 | return fmt.Errorf("-out must be specified") 51 | } 52 | 53 | blockReg := &merge.BlockRegistry{ 54 | BlockRegistry: world.DefaultBlockRegistry, 55 | Rids: make(map[uint32]merge.Block), 56 | } 57 | 58 | var worlds []worldInstance 59 | for _, worldName := range mergeSettings.InputWorlds { 60 | sp := strings.SplitN(worldName, ";", 3) 61 | worldName = sp[0] 62 | var offset world.ChunkPos 63 | if len(sp) == 3 { 64 | x, err := strconv.Atoi(sp[1]) 65 | if err != nil { 66 | return fmt.Errorf("%s %w", worldName, err) 67 | } 68 | z, err := strconv.Atoi(sp[2]) 69 | if err != nil { 70 | return fmt.Errorf("%s %w", worldName, err) 71 | } 72 | offset[0] = int32(x) 73 | offset[1] = int32(z) 74 | } 75 | db, err := mcdb.Config{ 76 | Log: slog.Default(), 77 | Blocks: blockReg, 78 | LDBOptions: &opt.Options{ 79 | ReadOnly: true, 80 | }, 81 | }.Open(utils.PathData(worldName)) 82 | if err != nil { 83 | return fmt.Errorf("%s %w", worldName, err) 84 | } 85 | defer db.Close() 86 | worlds = append(worlds, worldInstance{Name: worldName, db: db, offset: offset}) 87 | } 88 | 89 | if mergeSettings.Bounds { 90 | for _, w := range worlds { 91 | fmt.Printf("\n%s\n", w.Name) 92 | minBound, maxBound, err := w.getWorldBounds() 93 | if err != nil { 94 | return err 95 | } 96 | fmt.Printf("Min: %d,%d Max: %d,%d\n", minBound[0], minBound[1], maxBound[0], maxBound[1]) 97 | } 98 | return nil 99 | } 100 | 101 | outPath := utils.PathData(mergeSettings.OutPath) 102 | 103 | if _, err := os.Stat(outPath + "/level.dat"); err == nil { 104 | err = os.RemoveAll(outPath) 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | dbOut, err := mcdb.Config{ 110 | Log: slog.Default(), 111 | Blocks: blockReg, 112 | }.Open(outPath) 113 | if err != nil { 114 | return err 115 | } 116 | defer dbOut.Close() 117 | 118 | for _, w := range worlds { 119 | err = c.processWorld(w.db, dbOut, w.offset) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | 125 | ldat := worlds[0].db.LevelDat() 126 | *dbOut.LevelDat() = *ldat 127 | 128 | return nil 129 | } 130 | 131 | func (w worldInstance) getWorldBounds() (minChunk, maxChunk world.ChunkPos, err error) { 132 | minChunk = world.ChunkPos{math.MaxInt32, math.MaxInt32} 133 | maxChunk = world.ChunkPos{math.MinInt32, math.MinInt32} 134 | it := w.db.NewColumnIterator(nil) 135 | defer it.Release() 136 | for it.Next() { 137 | pos := it.Position() 138 | minChunk[0] = min(minChunk[0], pos[0]) 139 | minChunk[1] = min(minChunk[1], pos[1]) 140 | maxChunk[0] = max(maxChunk[0], pos[0]) 141 | maxChunk[1] = max(maxChunk[1], pos[1]) 142 | } 143 | 144 | if err := it.Error(); err != nil { 145 | return minChunk, maxChunk, err 146 | } 147 | 148 | return 149 | } 150 | 151 | func (c *MergeCMD) processWorld(db *mcdb.DB, out *mcdb.DB, offset world.ChunkPos) error { 152 | it := db.NewColumnIterator(nil) 153 | defer it.Release() 154 | for it.Next() { 155 | column := it.Column() 156 | pos := it.Position() 157 | dim := it.Dimension() 158 | pos[0] += offset[0] 159 | pos[1] += offset[1] 160 | for _, ent := range column.Entities { 161 | pos := ent.Data["Pos"].([]any) 162 | x := pos[0].(float32) 163 | y := pos[1].(float32) 164 | z := pos[2].(float32) 165 | ent.Data["Pos"] = []any{ 166 | x + float32(offset[0]*16), 167 | y, 168 | z + float32(offset[1]*16), 169 | } 170 | ent.Data["UniqueID"] = rand.Int64() 171 | } 172 | err := out.StoreColumn(pos, dim, column) 173 | if err != nil { 174 | return err 175 | } 176 | } 177 | if err := it.Error(); err != nil { 178 | return err 179 | } 180 | return nil 181 | } 182 | 183 | func init() { 184 | commands.RegisterCommand(&MergeCMD{}) 185 | } 186 | -------------------------------------------------------------------------------- /subcommands/realm-address.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bedrock-tool/bedrocktool/utils" 7 | "github.com/bedrock-tool/bedrocktool/utils/commands" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type RealmAddressSettings struct { 12 | Realm string `opt:"Realm Name" flag:"realm"` 13 | } 14 | 15 | type RealmAddressCMD struct{} 16 | 17 | func (RealmAddressCMD) Name() string { 18 | return "realm-address" 19 | } 20 | 21 | func (RealmAddressCMD) Description() string { 22 | return "gets realms address" 23 | } 24 | 25 | func (RealmAddressCMD) Settings() any { 26 | return new(RealmAddressSettings) 27 | } 28 | 29 | func (c *RealmAddressCMD) Run(ctx context.Context, settings any) error { 30 | realmSettings := settings.(*RealmAddressSettings) 31 | connectInfo := utils.ConnectInfo{Value: "realm:" + realmSettings.Realm} 32 | address, err := connectInfo.Address(ctx) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | logrus.Infof("Address: %s", address) 38 | return nil 39 | } 40 | 41 | func init() { 42 | commands.RegisterCommand(&RealmAddressCMD{}) 43 | } 44 | -------------------------------------------------------------------------------- /subcommands/realms-list.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/bedrock-tool/bedrocktool/locale" 8 | "github.com/bedrock-tool/bedrocktool/utils" 9 | "github.com/bedrock-tool/bedrocktool/utils/commands" 10 | "github.com/bedrock-tool/bedrocktool/utils/xbox" 11 | ) 12 | 13 | type RealmListCMD struct{} 14 | 15 | func (RealmListCMD) Name() string { 16 | return "list-realms" 17 | } 18 | 19 | func (RealmListCMD) Description() string { 20 | return locale.Loc("list_realms_synopsis", nil) 21 | } 22 | 23 | func (RealmListCMD) Settings() any { 24 | return nil 25 | } 26 | 27 | func (RealmListCMD) Run(ctx context.Context, settings any) error { 28 | if !utils.Auth.LoggedIn() { 29 | err := utils.Auth.Login(ctx, &xbox.DeviceTypeAndroid) 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | realms, err := utils.Auth.Realms().Realms(ctx) 35 | if err != nil { 36 | return err 37 | } 38 | for _, realm := range realms { 39 | fmt.Println(locale.Loc("realm_list_line", locale.Strmap{"Name": realm.Name, "Id": realm.ID})) 40 | } 41 | return nil 42 | } 43 | 44 | func init() { 45 | commands.RegisterCommand(&RealmListCMD{}) 46 | } 47 | -------------------------------------------------------------------------------- /subcommands/resourcepack-d/resourcepack-d.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bedrock-tool/bedrocktool/ff59f55cfa0cd02a4825976dd1b5e13c679b8a9a/subcommands/resourcepack-d/resourcepack-d.go -------------------------------------------------------------------------------- /subcommands/resourcepack-stub.go: -------------------------------------------------------------------------------- 1 | //go:build packs 2 | 3 | package subcommands 4 | 5 | import ( 6 | _ "github.com/bedrock-tool/bedrocktool/subcommands/resourcepack-d" 7 | ) 8 | -------------------------------------------------------------------------------- /subcommands/skins.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bedrock-tool/bedrocktool/handlers" 7 | "github.com/bedrock-tool/bedrocktool/locale" 8 | "github.com/bedrock-tool/bedrocktool/ui/messages" 9 | "github.com/bedrock-tool/bedrocktool/utils/commands" 10 | "github.com/bedrock-tool/bedrocktool/utils/proxy" 11 | ) 12 | 13 | type SkinSettings struct { 14 | ProxySettings proxy.ProxySettings 15 | 16 | Filter string `opt:"Name Filter" flag:"filter"` 17 | NoProxy bool `opt:"No Proxy" flag:"no-proxy"` 18 | } 19 | 20 | type SkinCMD struct { 21 | ServerAddress string 22 | ListenAddress string 23 | Filter string 24 | NoProxy bool 25 | EnableClientCache bool 26 | } 27 | 28 | func (SkinCMD) Name() string { 29 | return "skins" 30 | } 31 | 32 | func (SkinCMD) Description() string { 33 | return locale.Loc("skins_synopsis", nil) 34 | } 35 | 36 | func (SkinCMD) Settings() any { 37 | return new(SkinSettings) 38 | } 39 | 40 | func (SkinCMD) Run(ctx context.Context, settings any) error { 41 | skinSettings := settings.(*SkinSettings) 42 | 43 | p, err := proxy.New(ctx, skinSettings.ProxySettings) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | p.AddHandler(handlers.NewSkinSaver(func(sa handlers.SkinAdd) { 49 | messages.SendEvent(&messages.EventPlayerSkin{ 50 | PlayerName: sa.PlayerName, 51 | Skin: *sa.Skin, 52 | }) 53 | })) 54 | 55 | p.AddHandler(func() *proxy.Handler { 56 | return &proxy.Handler{ 57 | Name: "Skin CMD", 58 | OnConnect: func(_ *proxy.Session) bool { 59 | messages.SendEvent(&messages.EventSetUIState{ 60 | State: messages.UIStateMain, 61 | }) 62 | return false 63 | }, 64 | } 65 | }) 66 | 67 | return p.Run(!skinSettings.NoProxy) 68 | } 69 | 70 | func init() { 71 | commands.RegisterCommand(&SkinCMD{}) 72 | } 73 | -------------------------------------------------------------------------------- /subcommands/update.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bedrock-tool/bedrocktool/locale" 7 | "github.com/bedrock-tool/bedrocktool/utils" 8 | "github.com/bedrock-tool/bedrocktool/utils/commands" 9 | "github.com/bedrock-tool/bedrocktool/utils/updater" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type UpdateCMD struct{} 14 | 15 | func (UpdateCMD) Name() string { 16 | return "update" 17 | } 18 | func (UpdateCMD) Description() string { 19 | return locale.Loc("update_synopsis", nil) 20 | } 21 | func (UpdateCMD) Settings() any { 22 | return nil 23 | } 24 | 25 | func (c *UpdateCMD) Run(ctx context.Context, settings any) error { 26 | update, err := updater.UpdateAvailable() 27 | if err != nil { 28 | return err 29 | } 30 | isNew := update.Version != utils.Version 31 | if !isNew { 32 | logrus.Info(locale.Loc("no_update", nil)) 33 | return nil 34 | } 35 | logrus.Info(locale.Loc("updating", locale.Strmap{"Version": update.Version})) 36 | 37 | if err := updater.DoUpdate(); err != nil { 38 | return err 39 | } 40 | 41 | logrus.Info(locale.Loc("updated", nil)) 42 | return nil 43 | } 44 | 45 | func init() { 46 | commands.RegisterCommand(&UpdateCMD{}) 47 | } 48 | -------------------------------------------------------------------------------- /subcommands/worlds.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/bedrock-tool/bedrocktool/handlers/worlds" 8 | "github.com/bedrock-tool/bedrocktool/locale" 9 | "github.com/bedrock-tool/bedrocktool/utils/commands" 10 | "github.com/bedrock-tool/bedrocktool/utils/proxy" 11 | ) 12 | 13 | type WorldSettings struct { 14 | ProxySettings proxy.ProxySettings 15 | Void bool `opt:"Void Generator" flag:"void" default:"true" desc:"locale.enable_void"` 16 | Image bool `opt:"Image" flag:"image" desc:"locale.save_image"` 17 | Entities bool `opt:"Entities" flag:"save-entities" default:"true" desc:"Save Entities"` 18 | Inventories bool `opt:"Inventories" flag:"save-inventories" default:"true" desc:"Save Inventories"` 19 | BlockUpdates bool `opt:"Block Updates" flag:"block-updates" desc:"Block updates"` 20 | ExcludeMobs []string `opt:"Exclude Mobs" flag:"exclude-mobs" desc:"list of mobs to exclude seperated by comma"` 21 | StartPaused bool `opt:"Start Paused" flag:"start-paused" desc:"pause the capturing on startup (can be restarted using /start-capture ingame)"` 22 | PreloadedReplay string `opt:"Preload Replay" flag:"preload-replay" desc:"preload from a replay" type:"file,pcap2"` 23 | ChunkRadius int `opt:"Chunk Radius" flag:"chunk-radius" desc:"the max chunk radius to force"` 24 | ScriptPath string `opt:"Script Path" flag:"script" desc:"path to script to use" type:"file,js"` 25 | } 26 | 27 | type WorldCMD struct{} 28 | 29 | func (WorldCMD) Name() string { 30 | return "worlds" 31 | } 32 | 33 | func (WorldCMD) Description() string { 34 | return locale.Loc("world_synopsis", nil) 35 | } 36 | 37 | func (WorldCMD) Settings() any { 38 | return new(WorldSettings) 39 | } 40 | 41 | func (WorldCMD) Run(ctx context.Context, settings any) error { 42 | worldSettings := settings.(*WorldSettings) 43 | 44 | var scriptSource string 45 | if worldSettings.ScriptPath != "" { 46 | data, err := os.ReadFile(worldSettings.ScriptPath) 47 | if err != nil { 48 | return err 49 | } 50 | scriptSource = string(data) 51 | } 52 | 53 | p, err := proxy.New(ctx, worldSettings.ProxySettings) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | p.AddHandler(worlds.NewWorldsHandler(ctx, worlds.WorldSettings{ 59 | VoidGen: worldSettings.Void, 60 | SaveEntities: worldSettings.Entities, 61 | SaveInventories: worldSettings.Inventories, 62 | SaveImage: worldSettings.Image, 63 | ExcludedMobs: worldSettings.ExcludeMobs, 64 | StartPaused: worldSettings.StartPaused, 65 | PreloadReplay: worldSettings.PreloadedReplay, 66 | ChunkRadius: int32(worldSettings.ChunkRadius), 67 | Script: scriptSource, 68 | BlockUpdates: worldSettings.BlockUpdates, 69 | //Players: true, 70 | })) 71 | 72 | err = p.Run(true) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func init() { 81 | commands.RegisterCommand(&WorldCMD{}) 82 | } 83 | -------------------------------------------------------------------------------- /ui/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/bedrock-tool/bedrocktool/locale" 12 | "github.com/bedrock-tool/bedrocktool/ui/messages" 13 | "github.com/bedrock-tool/bedrocktool/utils" 14 | "github.com/bedrock-tool/bedrocktool/utils/commands" 15 | "github.com/bedrock-tool/bedrocktool/utils/updater" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | type CLI struct { 20 | IsInteractive bool 21 | } 22 | 23 | func (c *CLI) Init() error { 24 | messages.SetEventHandler(c.eventHandler) 25 | return nil 26 | } 27 | 28 | func printCommands() { 29 | fmt.Println(locale.Loc("available_commands", nil)) 30 | for name, cmd := range commands.Registered { 31 | fmt.Printf("\t%s\t%s\n\r", name, cmd.Description()) 32 | } 33 | } 34 | 35 | func (c *CLI) Start(ctx context.Context, cancel context.CancelCauseFunc) error { 36 | utils.Auth.SetHandler(nil) 37 | if !utils.IsDebug() { 38 | go updater.UpdateCheck(c) 39 | } 40 | 41 | if c.IsInteractive { 42 | select { 43 | case <-ctx.Done(): 44 | return nil 45 | default: 46 | printCommands() 47 | fmt.Println(locale.Loc("use_to_run_command", nil)) 48 | 49 | cmd, cancelled := utils.UserInput(ctx, locale.Loc("input_command", nil), func(s string) bool { 50 | for k := range commands.Registered { 51 | if s == k { 52 | return true 53 | } 54 | } 55 | return false 56 | }) 57 | if cancelled { 58 | cancel(errors.New("cancelled input")) 59 | return nil 60 | } 61 | _cmd := strings.Split(cmd, " ") 62 | os.Args = append(os.Args, _cmd...) 63 | } 64 | } 65 | 66 | var subcommandIndex int = -1 67 | for i := range len(os.Args[1:]) { 68 | arg := os.Args[1+i] 69 | if len(arg) == 0 { 70 | continue 71 | } 72 | if arg[0] == '-' { 73 | if !strings.ContainsRune(arg, '=') { 74 | i++ 75 | } 76 | continue 77 | } 78 | subcommandIndex = i 79 | break 80 | } 81 | if subcommandIndex == -1 { 82 | return fmt.Errorf("no command selected") 83 | } 84 | 85 | var args []string 86 | for i, arg := range os.Args[1:] { 87 | if subcommandIndex == i { 88 | continue 89 | } 90 | args = append(args, arg) 91 | } 92 | subcommandName := os.Args[1+subcommandIndex] 93 | 94 | subcommand, ok := commands.Registered[subcommandName] 95 | if !ok { 96 | logrus.Errorf("%s is not a known subcommand", subcommandName) 97 | printCommands() 98 | return nil 99 | } 100 | 101 | settings, flags, err := commands.ParseArgs(ctx, subcommand, args) 102 | if err != nil { 103 | if flags != nil { 104 | fmt.Printf("Usage for %s:\n", subcommandName) 105 | flags.PrintDefaults() 106 | } 107 | return err 108 | } 109 | 110 | err = subcommand.Run(ctx, settings) 111 | if err != nil { 112 | return err 113 | } 114 | cancel(nil) 115 | 116 | if c.IsInteractive { 117 | logrus.Info(locale.Loc("enter_to_exit", nil)) 118 | input := bufio.NewScanner(os.Stdin) 119 | input.Scan() 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func (c *CLI) eventHandler(event any) error { 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /ui/gui/guim/guim.go: -------------------------------------------------------------------------------- 1 | package guim 2 | 3 | import ( 4 | "gioui.org/layout" 5 | "gioui.org/x/explorer" 6 | ) 7 | 8 | type Guim interface { 9 | ShowPopup(popup any) 10 | ClosePopup(id string) 11 | StartSubcommand(subCommand string, settings any) 12 | ExitSubcommand() 13 | Invalidate() 14 | Error(err error) error 15 | Explorer() *explorer.Explorer 16 | OpenUrl(uri string) 17 | Toast(gtx layout.Context, t string) 18 | CloseLogs() 19 | } 20 | -------------------------------------------------------------------------------- /ui/gui/icons/main.go: -------------------------------------------------------------------------------- 1 | package icons 2 | 3 | import ( 4 | "gioui.org/widget" 5 | "golang.org/x/exp/shiny/materialdesign/icons" 6 | ) 7 | 8 | func mustIcon(data []byte) widget.Icon { 9 | ic, err := widget.NewIcon(data) 10 | if err != nil { 11 | panic(err) 12 | } 13 | return *ic 14 | } 15 | 16 | var ActionUpdate = mustIcon(icons.ActionUpdate) 17 | -------------------------------------------------------------------------------- /ui/gui/logger.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "bytes" 5 | "image/color" 6 | "io" 7 | "sync" 8 | 9 | "gioui.org/io/clipboard" 10 | "gioui.org/layout" 11 | "gioui.org/widget" 12 | "gioui.org/widget/material" 13 | "gioui.org/x/component" 14 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 15 | "github.com/bedrock-tool/bedrocktool/utils" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | type logger struct { 20 | g guim.Guim 21 | lines []*logrus.Entry 22 | l sync.Mutex 23 | list widget.List 24 | 25 | clickCopyLogs widget.Clickable 26 | clickClose widget.Clickable 27 | } 28 | 29 | type C = layout.Context 30 | type D = layout.Dimensions 31 | 32 | func (l *logger) Layout(gtx C, th *material.Theme) D { 33 | if l.clickCopyLogs.Clicked(gtx) { 34 | var logTxt []byte 35 | for _, line := range l.lines { 36 | lineBytes, _ := line.Bytes() 37 | logTxt = append(logTxt, utils.StripAnsiBytes(lineBytes)...) 38 | } 39 | gtx.Execute(clipboard.WriteCmd{ 40 | Type: "text", 41 | Data: io.NopCloser(bytes.NewReader(logTxt)), 42 | }) 43 | l.g.Toast(gtx, "Copied!") 44 | } 45 | 46 | if l.clickClose.Clicked(gtx) { 47 | l.g.CloseLogs() 48 | } 49 | 50 | gtx.Constraints.Min = gtx.Constraints.Max 51 | return layout.UniformInset(20).Layout(gtx, func(gtx C) D { 52 | component.Rect{ 53 | Color: color.NRGBA{A: 240}, 54 | Size: gtx.Constraints.Max, 55 | Radii: 15, 56 | }.Layout(gtx) 57 | return layout.UniformInset(8).Layout(gtx, func(gtx C) D { 58 | return layout.Stack{ 59 | Alignment: layout.N, 60 | }.Layout(gtx, 61 | layout.Expanded(func(gtx layout.Context) layout.Dimensions { 62 | return material.List(th, &l.list).Layout(gtx, len(l.lines), func(gtx C, index int) D { 63 | line := l.lines[index] 64 | t := material.Body1(th, line.Message) 65 | t.Color = color.NRGBA{0xff, 0xff, 0xff, 0xff} 66 | return t.Layout(gtx) 67 | }) 68 | }), 69 | layout.Stacked(func(gtx layout.Context) layout.Dimensions { 70 | return layout.Flex{ 71 | Axis: layout.Vertical, 72 | Spacing: layout.SpaceBetween, 73 | Alignment: layout.End, 74 | }.Layout(gtx, 75 | layout.Flexed(1, layout.Spacer{Height: 10000}.Layout), 76 | layout.Rigid(func(gtx layout.Context) layout.Dimensions { 77 | return layout.Flex{ 78 | Axis: layout.Horizontal, 79 | }.Layout(gtx, 80 | layout.Rigid(material.Button(th, &l.clickCopyLogs, "Copy Logs").Layout), 81 | layout.Rigid(layout.Spacer{Width: 8}.Layout), 82 | layout.Rigid(material.Button(th, &l.clickClose, "Close").Layout), 83 | ) 84 | }), 85 | ) 86 | }), 87 | ) 88 | }) 89 | }) 90 | 91 | } 92 | 93 | func (l *logger) Levels() []logrus.Level { 94 | return []logrus.Level{ 95 | logrus.ErrorLevel, 96 | logrus.WarnLevel, 97 | logrus.InfoLevel, 98 | logrus.DebugLevel, 99 | } 100 | } 101 | 102 | func (l *logger) Fire(e *logrus.Entry) error { 103 | l.l.Lock() 104 | l.lines = append(l.lines, e) 105 | l.l.Unlock() 106 | l.g.Invalidate() 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /ui/gui/mctext/mctext.go: -------------------------------------------------------------------------------- 1 | package mctext 2 | 3 | import ( 4 | "image/color" 5 | "math/rand/v2" 6 | "regexp" 7 | 8 | "gioui.org/font" 9 | "gioui.org/layout" 10 | "gioui.org/unit" 11 | "gioui.org/widget/material" 12 | "gioui.org/x/styledtext" 13 | ) 14 | 15 | var splitter = regexp.MustCompile("((?:§.)?(?:[^§]+)?)") 16 | 17 | func Label(th *material.Theme, size unit.Sp, txt string, invalidate func(), frame int) func(gtx layout.Context) layout.Dimensions { 18 | split := splitter.FindAllString(txt, -1) 19 | var Styles []styledtext.SpanStyle 20 | 21 | var activeColor color.NRGBA = th.Fg 22 | var bold bool 23 | var italic bool 24 | var obfuscated bool 25 | 26 | for _, part := range split { 27 | if len(part) == 0 { 28 | continue 29 | } 30 | partR := []rune(part) 31 | if partR[0] == '§' { 32 | switch partR[1] { 33 | case '0': 34 | activeColor = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff} 35 | case '1': 36 | activeColor = color.NRGBA{R: 0x00, G: 0x00, B: 0xAA, A: 0xff} 37 | case '2': 38 | activeColor = color.NRGBA{R: 0x00, G: 0xAA, B: 0x00, A: 0xff} 39 | case '3': 40 | activeColor = color.NRGBA{R: 0x00, G: 0xAA, B: 0xAA, A: 0xff} 41 | case '4': 42 | activeColor = color.NRGBA{R: 0xAA, G: 0x00, B: 0x00, A: 0xff} 43 | case '5': 44 | activeColor = color.NRGBA{R: 0xAA, G: 0x00, B: 0xAA, A: 0xff} 45 | case '6': 46 | activeColor = color.NRGBA{R: 0xFF, G: 0xAA, B: 0x00, A: 0xff} 47 | case '7': 48 | activeColor = color.NRGBA{R: 0xc6, G: 0xc6, B: 0xc6, A: 0xff} 49 | case '8': 50 | activeColor = color.NRGBA{R: 0x55, G: 0x55, B: 0x55, A: 0xff} 51 | case '9': 52 | activeColor = color.NRGBA{R: 0x55, G: 0x55, B: 0xff, A: 0xff} 53 | case 'a': 54 | activeColor = color.NRGBA{R: 0x55, G: 0xff, B: 0x55, A: 0xff} 55 | case 'b': 56 | activeColor = color.NRGBA{R: 0x55, G: 0xff, B: 0xff, A: 0xff} 57 | case 'c': 58 | activeColor = color.NRGBA{R: 0xff, G: 0x55, B: 0x55, A: 0xff} 59 | case 'd': 60 | activeColor = color.NRGBA{R: 0xff, G: 0x55, B: 0xff, A: 0xff} 61 | case 'e': 62 | activeColor = color.NRGBA{R: 0xff, G: 0xff, B: 0x55, A: 0xff} 63 | case 'f': 64 | activeColor = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} 65 | case 'g': 66 | activeColor = color.NRGBA{R: 0xDD, G: 0xD6, B: 0x05, A: 0xff} 67 | case 'h': 68 | activeColor = color.NRGBA{R: 0xE3, G: 0xD4, B: 0xD1, A: 0xff} 69 | case 'i': 70 | activeColor = color.NRGBA{R: 0xCE, G: 0xCA, B: 0xCA, A: 0xff} 71 | case 'j': 72 | activeColor = color.NRGBA{R: 0x44, G: 0x3A, B: 0x3B, A: 0xff} 73 | case 'm': 74 | activeColor = color.NRGBA{R: 0x97, G: 0x16, B: 0x07, A: 0xff} 75 | case 'n': 76 | activeColor = color.NRGBA{R: 0xB4, G: 0x68, B: 0x4D, A: 0xff} 77 | case 'p': 78 | activeColor = color.NRGBA{R: 0xDE, G: 0xB1, B: 0x2D, A: 0xff} 79 | case 'q': 80 | activeColor = color.NRGBA{R: 0x97, G: 0xA0, B: 0x36, A: 0xff} 81 | case 's': 82 | activeColor = color.NRGBA{R: 0x2C, G: 0xBA, B: 0xA8, A: 0xff} 83 | case 't': 84 | activeColor = color.NRGBA{R: 0x21, G: 0x49, B: 0x7B, A: 0xff} 85 | case 'u': 86 | activeColor = color.NRGBA{R: 0x21, G: 0x49, B: 0x7B, A: 0xff} 87 | case 'r': 88 | activeColor = th.Fg 89 | bold, italic, obfuscated = false, false, false 90 | case 'l': 91 | bold = true 92 | case 'o': 93 | italic = true 94 | case 'k': 95 | obfuscated = true 96 | } 97 | partR = partR[2:] 98 | _ = obfuscated 99 | } 100 | 101 | if len(partR) == 0 { 102 | continue 103 | } 104 | 105 | var fontStyle font.Style = font.Regular 106 | if italic { 107 | fontStyle = font.Italic 108 | } 109 | var fontWeight font.Weight = font.Normal 110 | if bold { 111 | fontWeight = font.Bold 112 | } 113 | 114 | if obfuscated { 115 | obfuscatedDict := []rune{'a', 'b', 'c', 'd'} 116 | r := rand.New(rand.NewPCG(0, uint64(frame/3))) 117 | for i := 0; i < len(partR); i++ { 118 | partR[i] = obfuscatedDict[r.IntN(len(obfuscatedDict))] 119 | } 120 | invalidate() 121 | } 122 | 123 | Styles = append(Styles, styledtext.SpanStyle{ 124 | Font: font.Font{ 125 | Typeface: th.Face, 126 | Style: fontStyle, 127 | Weight: fontWeight, 128 | }, 129 | Size: size, 130 | Color: activeColor, 131 | Content: string(partR), 132 | }) 133 | } 134 | 135 | return func(gtx layout.Context) layout.Dimensions { 136 | return styledtext.TextStyle{ 137 | Styles: Styles, 138 | Shaper: th.Shaper, 139 | }.Layout(gtx, nil) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /ui/gui/pages/page.go: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "image/color" 5 | 6 | "gioui.org/layout" 7 | "gioui.org/text" 8 | "gioui.org/widget" 9 | "gioui.org/widget/material" 10 | "gioui.org/x/component" 11 | "github.com/bedrock-tool/bedrocktool/ui/messages" 12 | ) 13 | 14 | type Page interface { 15 | ID() string 16 | Actions(th *material.Theme) []component.AppBarAction 17 | Overflow() []component.OverflowAction 18 | Layout(gtx layout.Context, th *material.Theme) layout.Dimensions 19 | NavItem() component.NavItem 20 | messages.EventHandler 21 | } 22 | 23 | var Pages = map[string]func(*Router) Page{} 24 | 25 | func Register(name string, fun func(*Router) Page) { 26 | Pages[name] = fun 27 | } 28 | 29 | func AppBarSwitch(toggle *widget.Bool, label string, th **material.Theme) component.AppBarAction { 30 | return component.AppBarAction{ 31 | Layout: func(gtx layout.Context, bg, fg color.NRGBA) layout.Dimensions { 32 | return layout.UniformInset(5).Layout(gtx, func(gtx layout.Context) layout.Dimensions { 33 | return layout.Flex{ 34 | Axis: layout.Horizontal, 35 | Alignment: layout.Middle, 36 | }.Layout(gtx, 37 | layout.Rigid(material.Switch(*th, toggle, label).Layout), 38 | layout.Rigid(func(gtx layout.Context) layout.Dimensions { 39 | l := material.Label(*th, 12, label) 40 | l.Alignment = text.Middle 41 | return layout.UniformInset(5).Layout(gtx, l.Layout) 42 | }), 43 | ) 44 | }) 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/gui/pages/settings/address-input.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "gioui.org/layout" 5 | "gioui.org/op" 6 | "gioui.org/unit" 7 | "gioui.org/widget" 8 | "gioui.org/widget/material" 9 | "gioui.org/x/component" 10 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 11 | "github.com/bedrock-tool/bedrocktool/ui/gui/popups" 12 | "github.com/bedrock-tool/bedrocktool/utils" 13 | "github.com/bedrock-tool/bedrocktool/utils/discovery" 14 | "github.com/sandertv/gophertunnel/minecraft/realms" 15 | ) 16 | 17 | type addressInput struct { 18 | g guim.Guim 19 | editor widget.Editor 20 | showRealmsList widget.Clickable 21 | showGatherings widget.Clickable 22 | 23 | connectInfo *utils.ConnectInfo 24 | } 25 | 26 | var AddressInput = &addressInput{ 27 | g: nil, 28 | editor: widget.Editor{ 29 | SingleLine: true, 30 | }, 31 | connectInfo: &utils.ConnectInfo{}, 32 | } 33 | 34 | func (a *addressInput) SetGuim(g guim.Guim) { 35 | a.g = g 36 | } 37 | 38 | func (a *addressInput) GetConnectInfo() *utils.ConnectInfo { 39 | t := a.editor.Text() 40 | if len(t) == 0 { 41 | return nil 42 | } 43 | a.connectInfo.Value = t 44 | return a.connectInfo 45 | } 46 | 47 | func (a *addressInput) Layout(gtx C, th *material.Theme) D { 48 | if a.showRealmsList.Clicked(gtx) { 49 | a.g.ShowPopup(popups.NewRealmsList(a.g, func(realm *realms.Realm) { 50 | a.connectInfo.SetRealm(realm) 51 | a.editor.SetText(a.connectInfo.Value) 52 | })) 53 | } 54 | 55 | if a.showGatherings.Clicked(gtx) { 56 | a.g.ShowPopup(popups.NewGatherings(a.g, func(gathering *discovery.Gathering) { 57 | a.connectInfo.SetGathering(gathering) 58 | a.editor.SetText(a.connectInfo.Value) 59 | })) 60 | } 61 | 62 | return layout.UniformInset(5).Layout(gtx, func(gtx C) D { 63 | return layout.Flex{ 64 | Axis: layout.Vertical, 65 | }.Layout(gtx, 66 | layout.Rigid(func(gtx C) D { 67 | macro := op.Record(gtx.Ops) 68 | d := layout.UniformInset(8).Layout(gtx, func(gtx C) D { 69 | e := material.Editor(th, &a.editor, "Enter Server Address") 70 | e.LineHeight += 4 71 | return e.Layout(gtx) 72 | }) 73 | c := macro.Stop() 74 | component.Rect{ 75 | Color: component.WithAlpha(th.Fg, 10), 76 | Size: d.Size, 77 | Radii: 8, 78 | }.Layout(gtx) 79 | c.Add(gtx.Ops) 80 | return d 81 | }), 82 | layout.Rigid(func(gtx C) D { 83 | gtx.Constraints.Max.X = gtx.Dp(unit.Dp(200)) 84 | return layout.Flex{ 85 | Axis: layout.Horizontal, 86 | WeightSum: 2, 87 | }.Layout(gtx, 88 | layout.Flexed(1, func(gtx C) D { 89 | return layout.Inset{ 90 | Top: 5, 91 | Bottom: 5, 92 | Left: 0, 93 | Right: 5, 94 | }.Layout(gtx, material.Button(th, &a.showRealmsList, "Realms").Layout) 95 | }), 96 | layout.Flexed(1, func(gtx C) D { 97 | return layout.Inset{ 98 | Top: 5, 99 | Bottom: 5, 100 | Left: 0, 101 | Right: 5, 102 | }.Layout(gtx, material.Button(th, &a.showGatherings, "Events").Layout) 103 | }), 104 | ) 105 | }), 106 | ) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /ui/gui/pages/settings/file-input.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "os" 5 | 6 | "gioui.org/layout" 7 | "gioui.org/widget" 8 | "gioui.org/widget/material" 9 | "gioui.org/x/component" 10 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type fileInputWidget struct { 15 | g guim.Guim 16 | Hint string 17 | Ext string 18 | 19 | button widget.Clickable 20 | textField component.TextField 21 | } 22 | 23 | func (f *fileInputWidget) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { 24 | if f.button.Clicked(gtx) { 25 | go func() { 26 | var exts []string 27 | if f.Ext != "" { 28 | exts = append(exts, "."+f.Ext) 29 | } 30 | fp, err := f.g.Explorer().ChooseFile(exts...) 31 | if err != nil { 32 | logrus.Error(err) 33 | return 34 | } 35 | file := fp.(*os.File) 36 | f.textField.SetText(file.Name()) 37 | file.Close() 38 | }() 39 | } 40 | 41 | f.textField.Update(gtx, th, f.Hint) 42 | 43 | return layout.Flex{ 44 | Axis: layout.Horizontal, 45 | Alignment: layout.Middle, 46 | }.Layout(gtx, 47 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { 48 | return f.textField.Layout(gtx, th, f.Hint) 49 | }), 50 | layout.Rigid(func(gtx layout.Context) layout.Dimensions { 51 | button := material.Button(th, &f.button, "select file") 52 | return layout.Inset{ 53 | Top: 8, 54 | Left: 8, 55 | }.Layout(gtx, button.Layout) 56 | }), 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /ui/gui/pages/skins/skins.go: -------------------------------------------------------------------------------- 1 | package skins 2 | 3 | import ( 4 | "sync" 5 | 6 | "gioui.org/layout" 7 | "gioui.org/unit" 8 | "gioui.org/widget" 9 | "gioui.org/widget/material" 10 | "gioui.org/x/component" 11 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 12 | "github.com/bedrock-tool/bedrocktool/ui/gui/pages" 13 | "github.com/bedrock-tool/bedrocktool/ui/messages" 14 | "github.com/sandertv/gophertunnel/minecraft/protocol" 15 | ) 16 | 17 | type ( 18 | C = layout.Context 19 | D = layout.Dimensions 20 | ) 21 | 22 | type skin struct { 23 | PlayerName string 24 | Skin *protocol.Skin 25 | } 26 | 27 | const ID = "skins" 28 | 29 | type Page struct { 30 | g guim.Guim 31 | 32 | Skins []skin 33 | l sync.Mutex 34 | State messages.UIState 35 | SkinsList widget.List 36 | back widget.Clickable 37 | } 38 | 39 | func New(g guim.Guim) pages.Page { 40 | return &Page{ 41 | g: g, 42 | 43 | SkinsList: widget.List{ 44 | List: layout.List{ 45 | Axis: layout.Vertical, 46 | }, 47 | }, 48 | } 49 | } 50 | 51 | var _ pages.Page = &Page{} 52 | 53 | func (p *Page) ID() string { 54 | return ID 55 | } 56 | 57 | func (p *Page) Actions(th *material.Theme) []component.AppBarAction { 58 | return []component.AppBarAction{} 59 | } 60 | 61 | func (p *Page) Overflow() []component.OverflowAction { 62 | return []component.OverflowAction{} 63 | } 64 | 65 | func (p *Page) NavItem() component.NavItem { 66 | return component.NavItem{ 67 | Name: "Skin Grabber", 68 | //Icon: icon.OtherIcon, 69 | } 70 | } 71 | 72 | func (p *Page) Layout(gtx C, th *material.Theme) D { 73 | if p.back.Clicked(gtx) { 74 | p.g.ExitSubcommand() 75 | } 76 | 77 | return layout.Inset{ 78 | Top: unit.Dp(25), 79 | Bottom: unit.Dp(25), 80 | Right: unit.Dp(35), 81 | Left: unit.Dp(35), 82 | }.Layout(gtx, func(gtx C) D { 83 | return layout.Flex{ 84 | Axis: layout.Vertical, 85 | Spacing: layout.SpaceBetween, 86 | }.Layout(gtx, 87 | layout.Flexed(0.9, func(gtx C) D { 88 | // show the main ui 89 | return layout.Flex{ 90 | Axis: layout.Vertical, 91 | }.Layout(gtx, 92 | layout.Flexed(1, func(gtx C) D { 93 | p.l.Lock() 94 | defer p.l.Unlock() 95 | return material.List(th, &p.SkinsList).Layout(gtx, len(p.Skins), func(gtx C, index int) D { 96 | entry := p.Skins[len(p.Skins)-index-1] 97 | return layout.UniformInset(25).Layout(gtx, func(gtx C) D { 98 | return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, 99 | layout.Rigid(material.Label(th, th.TextSize, entry.PlayerName).Layout), 100 | ) 101 | }) 102 | }) 103 | }), 104 | ) 105 | }), 106 | layout.Flexed(0.1, func(gtx C) D { 107 | return layout.UniformInset(5).Layout(gtx, func(gtx C) D { 108 | gtx.Constraints.Max.Y = gtx.Dp(40) 109 | gtx.Constraints.Max.X = gtx.Constraints.Max.X / 6 110 | return material.Button(th, &p.back, "Return").Layout(gtx) 111 | }) 112 | }), 113 | ) 114 | }) 115 | } 116 | 117 | func (*Page) HaveFinishScreen() bool { 118 | return true 119 | } 120 | 121 | func (p *Page) HandleEvent(event any) error { 122 | switch event := event.(type) { 123 | case *messages.EventSetUIState: 124 | p.State = event.State 125 | 126 | case *messages.EventPlayerSkin: 127 | p.l.Lock() 128 | p.Skins = append(p.Skins, skin{ 129 | PlayerName: event.PlayerName, 130 | Skin: &event.Skin, 131 | }) 132 | p.l.Unlock() 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /ui/gui/pages/toast.go: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "time" 7 | 8 | "gioui.org/layout" 9 | "gioui.org/op" 10 | "gioui.org/op/clip" 11 | "gioui.org/op/paint" 12 | "gioui.org/unit" 13 | "gioui.org/widget/material" 14 | ) 15 | 16 | type Toast struct { 17 | Message string 18 | Visible bool 19 | StartTime time.Time 20 | Duration time.Duration 21 | } 22 | 23 | func (t *Toast) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { 24 | if !t.Visible { 25 | return layout.Dimensions{} 26 | } 27 | 28 | bgColor := color.NRGBA{R: 0x33, G: 0x33, B: 0x33, A: 0xFF} 29 | textColor := color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} 30 | padding := unit.Dp(12) 31 | cornerRadius := unit.Dp(8) 32 | 33 | return layout.Inset{ 34 | Top: padding, 35 | Bottom: padding, 36 | Left: padding, 37 | Right: padding, 38 | }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 39 | rec := op.Record(gtx.Ops) 40 | dims := layout.UniformInset(cornerRadius).Layout(gtx, func(gtx layout.Context) layout.Dimensions { 41 | gtx.Constraints.Min = image.Pt(0, 0) 42 | label := material.Body1(th, t.Message) 43 | label.Color = textColor 44 | return label.Layout(gtx) 45 | }) 46 | rep := rec.Stop() 47 | 48 | toastRect := clip.RRect{ 49 | Rect: image.Rect(0, 0, dims.Size.X, dims.Size.Y), 50 | SE: int(cornerRadius), 51 | SW: int(cornerRadius), 52 | NW: int(cornerRadius), 53 | NE: int(cornerRadius), 54 | }.Push(gtx.Ops) 55 | paint.ColorOp{Color: bgColor}.Add(gtx.Ops) 56 | paint.PaintOp{}.Add(gtx.Ops) 57 | rep.Add(gtx.Ops) 58 | toastRect.Pop() 59 | return dims 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /ui/gui/pages/update/update.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gioui.org/layout" 7 | "gioui.org/widget/material" 8 | "gioui.org/x/component" 9 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 10 | "github.com/bedrock-tool/bedrocktool/ui/gui/pages" 11 | "github.com/bedrock-tool/bedrocktool/ui/messages" 12 | ) 13 | 14 | type ( 15 | C = layout.Context 16 | D = layout.Dimensions 17 | ) 18 | 19 | const ID = "update" 20 | 21 | type Page struct { 22 | g guim.Guim 23 | 24 | percentDownload int 25 | } 26 | 27 | func New(g guim.Guim) pages.Page { 28 | return &Page{ 29 | g: g, 30 | } 31 | } 32 | 33 | func (p *Page) Actions(th *material.Theme) []component.AppBarAction { 34 | return nil 35 | } 36 | 37 | func (p *Page) HandleEvent(event any) error { 38 | switch event := event.(type) { 39 | case *messages.EventUpdateDownloadProgress: 40 | p.percentDownload = event.Progress 41 | } 42 | return nil 43 | } 44 | 45 | func (p *Page) ID() string { 46 | return ID 47 | } 48 | 49 | func (p *Page) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { 50 | return material.Body1(th, fmt.Sprintf("downloading (%d%%)", p.percentDownload)).Layout(gtx) 51 | } 52 | 53 | func (p *Page) NavItem() component.NavItem { 54 | return component.NavItem{} 55 | } 56 | 57 | func (p *Page) Overflow() []component.OverflowAction { 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /ui/gui/pages/worlds/map2.go: -------------------------------------------------------------------------------- 1 | package worlds 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "math" 7 | "sync" 8 | 9 | "gioui.org/f32" 10 | "gioui.org/io/event" 11 | "gioui.org/io/pointer" 12 | "gioui.org/op" 13 | "gioui.org/op/clip" 14 | "gioui.org/op/paint" 15 | "gioui.org/widget" 16 | "github.com/bedrock-tool/bedrocktool/ui/messages" 17 | "github.com/go-gl/mathgl/mgl32" 18 | "github.com/sandertv/gophertunnel/minecraft/protocol" 19 | ) 20 | 21 | const tileSize = 256 22 | 23 | type mapInput struct { 24 | click f32.Point 25 | scaleFactor float64 26 | center f32.Point 27 | transform f32.Affine2D 28 | grabbed bool 29 | cursor image.Point 30 | FollowPlayer widget.Bool 31 | playerPosition mgl32.Vec3 32 | } 33 | 34 | type Map2 struct { 35 | mapInput mapInput 36 | 37 | tileImages map[image.Point]*image.RGBA 38 | imageOps map[image.Point]paint.ImageOp 39 | l sync.Mutex 40 | } 41 | 42 | func (m *mapInput) HandlePointerEvent(e pointer.Event) { 43 | const WHEEL_DELTA = 120 44 | 45 | switch e.Kind { 46 | case pointer.Press: 47 | m.click = e.Position 48 | m.grabbed = true 49 | case pointer.Drag: 50 | m.transform = m.transform.Offset(e.Position.Sub(m.click)) 51 | m.click = e.Position 52 | case pointer.Release: 53 | m.grabbed = false 54 | case pointer.Scroll: 55 | if int(e.Scroll.Y)%WHEEL_DELTA == 0 { 56 | e.Scroll.Y = -8 * e.Scroll.Y / WHEEL_DELTA 57 | } 58 | scaleFactor := math.Pow(1.01, float64(e.Scroll.Y)) 59 | m.transform = m.transform.Scale(e.Position.Sub(m.center), f32.Pt(float32(scaleFactor), float32(scaleFactor))) 60 | m.scaleFactor *= scaleFactor 61 | } 62 | } 63 | 64 | func (m *mapInput) Layout(gtx C) func() { 65 | if m.scaleFactor == 0 { 66 | m.scaleFactor = 1 67 | } 68 | m.center = f32.Pt(float32(gtx.Constraints.Max.X), float32(gtx.Constraints.Max.Y)).Div(2) 69 | 70 | //size := gtx.Constraints.Max 71 | 72 | event.Op(gtx.Ops, m) 73 | for { 74 | ev, ok := gtx.Event(pointer.Filter{ 75 | Target: m, 76 | Kinds: pointer.Scroll | pointer.Drag | pointer.Press | pointer.Release, 77 | ScrollY: pointer.ScrollRange{ 78 | Min: -120, 79 | Max: 120, 80 | }, 81 | }) 82 | if !ok { 83 | break 84 | } 85 | m.HandlePointerEvent(ev.(pointer.Event)) 86 | } 87 | 88 | /* 89 | if m.FollowPlayer.Value { 90 | } 91 | */ 92 | 93 | return func() { 94 | if m.cursor.In(image.Rectangle(gtx.Constraints)) { 95 | if m.grabbed { 96 | pointer.CursorGrabbing.Add(gtx.Ops) 97 | } else { 98 | pointer.CursorGrab.Add(gtx.Ops) 99 | } 100 | } 101 | } 102 | } 103 | 104 | func (m *Map2) Layout(gtx C) D { 105 | defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() 106 | defer m.mapInput.Layout(gtx)() 107 | m.l.Lock() 108 | defer m.l.Unlock() 109 | 110 | for p, imageOp := range m.imageOps { 111 | scaledSize := tileSize * m.mapInput.scaleFactor 112 | pt := f32.Pt(float32(float64(p.X)*scaledSize), float32(float64(p.Y)*scaledSize)) 113 | 114 | // check if this needs to be drawn 115 | if (image.Rectangle{Max: gtx.Constraints.Max}).Intersect( 116 | image.Rectangle{ 117 | Min: pt.Round(), 118 | Max: pt.Add(f32.Pt(float32(scaledSize), float32(scaledSize))).Round(), 119 | }.Add(m.mapInput.center.Round()).Add(m.mapInput.transform.Transform(f32.Pt(0, 0)).Round()), 120 | ).Empty() { 121 | continue 122 | } 123 | 124 | aff := op.Affine(m.mapInput.transform.Offset(m.mapInput.center).Offset(pt)).Push(gtx.Ops) 125 | imageOp.Add(gtx.Ops) 126 | paint.PaintOp{}.Add(gtx.Ops) 127 | aff.Pop() 128 | } 129 | 130 | return D{Size: gtx.Constraints.Max} 131 | } 132 | 133 | func chunkPosToTilePos(cp protocol.ChunkPos) (tile image.Point, offset image.Point) { 134 | blockX := int(cp.X()) * 16 135 | blockY := int(cp.Z()) * 16 136 | tile.X = blockX / tileSize 137 | tile.Y = blockY / tileSize 138 | 139 | offset.X = blockX % tileSize 140 | offset.Y = blockY % tileSize 141 | 142 | if blockX < 0 && offset.X != 0 { 143 | tile.X-- 144 | offset.X += tileSize 145 | } 146 | if blockY < 0 && offset.Y != 0 { 147 | tile.Y-- 148 | offset.Y += tileSize 149 | } 150 | 151 | return 152 | } 153 | 154 | func (m *Map2) AddTiles(tiles []messages.MapTile) { 155 | var updatedTiles []image.Point 156 | for _, mapTile := range tiles { 157 | tilePos, posInTile := chunkPosToTilePos(mapTile.Pos) 158 | tileImg, ok := m.tileImages[tilePos] 159 | if !ok { 160 | tileImg = image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) 161 | m.tileImages[tilePos] = tileImg 162 | } 163 | draw.Draw(tileImg, image.Rectangle{ 164 | Min: posInTile, Max: posInTile.Add(image.Pt(16, 16)), 165 | }, &mapTile.Img, image.Point{}, draw.Src) 166 | updatedTiles = append(updatedTiles, tilePos) 167 | } 168 | 169 | for _, p := range updatedTiles { 170 | op := paint.NewImageOp(m.tileImages[p]) 171 | op.Filter = paint.FilterNearest 172 | m.imageOps[p] = op 173 | } 174 | } 175 | 176 | func (m *Map2) Reset() { 177 | m.l.Lock() 178 | defer m.l.Unlock() 179 | m.tileImages = make(map[image.Point]*image.RGBA) 180 | m.imageOps = make(map[image.Point]paint.ImageOp) 181 | } 182 | -------------------------------------------------------------------------------- /ui/gui/pages/worlds/map2_test.go: -------------------------------------------------------------------------------- 1 | package worlds 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "testing" 7 | 8 | "github.com/sandertv/gophertunnel/minecraft/protocol" 9 | ) 10 | 11 | func Test_chunkPosToTilePos(t *testing.T) { 12 | type test struct { 13 | pos protocol.ChunkPos 14 | expectedTile image.Point 15 | expectedOffset image.Point 16 | } 17 | 18 | const chunksPerTile = tileSize / 16 19 | 20 | var tests = []test{ 21 | { 22 | pos: protocol.ChunkPos{0, 0}, 23 | expectedTile: image.Pt(0, 0), 24 | expectedOffset: image.Pt(0, 0), 25 | }, 26 | { 27 | pos: protocol.ChunkPos{1, 0}, 28 | expectedTile: image.Pt(0, 0), 29 | expectedOffset: image.Pt(16, 0), 30 | }, 31 | { 32 | pos: protocol.ChunkPos{2, 0}, 33 | expectedTile: image.Pt(0, 0), 34 | expectedOffset: image.Pt(32, 0), 35 | }, 36 | { 37 | pos: protocol.ChunkPos{1, 1}, 38 | expectedTile: image.Pt(0, 0), 39 | expectedOffset: image.Pt(16, 16), 40 | }, 41 | { 42 | pos: protocol.ChunkPos{chunksPerTile, 1}, 43 | expectedTile: image.Pt(1, 0), 44 | expectedOffset: image.Pt(0, 16), 45 | }, 46 | { 47 | pos: protocol.ChunkPos{-1, 1}, 48 | expectedTile: image.Pt(-1, 0), 49 | expectedOffset: image.Pt(tileSize-16, 16), 50 | }, 51 | { 52 | pos: protocol.ChunkPos{-1, -1}, 53 | expectedTile: image.Pt(-1, -1), 54 | expectedOffset: image.Pt(tileSize-16, tileSize-16), 55 | }, 56 | { 57 | pos: protocol.ChunkPos{-2, -1}, 58 | expectedTile: image.Pt(-1, -1), 59 | expectedOffset: image.Pt(tileSize-32, tileSize-16), 60 | }, 61 | } 62 | 63 | for _, t2 := range tests { 64 | tile, offset := chunkPosToTilePos(t2.pos) 65 | if t2.expectedOffset != offset { 66 | t.Error(fmt.Errorf("%+v wrong offset %v", t2, offset)) 67 | } 68 | if t2.expectedTile != tile { 69 | t.Error(fmt.Errorf("%+v wrong tile %v", t2, tile)) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ui/gui/popups/authpopup.go: -------------------------------------------------------------------------------- 1 | package popups 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "image/color" 7 | "io" 8 | 9 | "gioui.org/io/clipboard" 10 | "gioui.org/layout" 11 | "gioui.org/widget" 12 | "gioui.org/widget/material" 13 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 14 | "github.com/bedrock-tool/bedrocktool/utils" 15 | ) 16 | 17 | type guiAuth struct { 18 | g guim.Guim 19 | uri string 20 | uriDest string 21 | clickUri widget.Clickable 22 | clickCode widget.Clickable 23 | code string 24 | codeSelect widget.Selectable 25 | close widget.Clickable 26 | } 27 | 28 | func NewGuiAuth(g guim.Guim, uri, code string) *guiAuth { 29 | uriDest := fmt.Sprintf("https://login.live.com/oauth20_remoteconnect.srf?otc=%s", code) 30 | return &guiAuth{g: g, uri: uri, uriDest: uriDest, code: code} 31 | } 32 | 33 | func (p *guiAuth) HandleEvent(event any) error { 34 | return nil 35 | } 36 | 37 | func (guiAuth) ID() string { 38 | return "ms-auth" 39 | } 40 | 41 | func (guiAuth) Close() error { 42 | return nil 43 | } 44 | 45 | func (g *guiAuth) Layout(gtx C, th *material.Theme) D { 46 | if g.clickUri.Clicked(gtx) { 47 | g.g.OpenUrl(g.uriDest) 48 | } 49 | if g.clickCode.Clicked(gtx) { 50 | gtx.Execute(clipboard.WriteCmd{ 51 | Type: "text", 52 | Data: io.NopCloser(bytes.NewReader([]byte(g.code))), 53 | }) 54 | g.g.Toast(gtx, "Copied!") 55 | } 56 | 57 | if g.close.Clicked(gtx) { 58 | utils.CancelLogin() 59 | g.g.ClosePopup(g.ID()) 60 | } 61 | 62 | return LayoutPopupBackground(gtx, th, "guiAuth", func(gtx C) D { 63 | return layout.Flex{ 64 | Axis: layout.Vertical, 65 | }.Layout(gtx, 66 | layout.Flexed(1, func(gtx C) D { 67 | return layout.Center.Layout(gtx, func(gtx C) D { 68 | return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, 69 | layout.Rigid(material.Body1(th, "Authenticate at: ").Layout), 70 | layout.Rigid(func(gtx C) D { 71 | uri := material.Body1(th, g.uri) 72 | uri.Color = color.NRGBA{R: 0x06, G: 0x4c, B: 0xa6, A: 0xff} 73 | return material.Clickable(gtx, &g.clickUri, uri.Layout) 74 | }), 75 | layout.Rigid(func(gtx C) D { 76 | return layout.Flex{ 77 | Axis: layout.Horizontal, 78 | }.Layout(gtx, 79 | layout.Rigid(material.Body1(th, "Using Code: ").Layout), 80 | layout.Rigid(func(gtx C) D { 81 | t := material.Body1(th, g.code) 82 | t.State = &g.codeSelect 83 | return material.Clickable(gtx, &g.clickCode, t.Layout) 84 | }), 85 | ) 86 | }), 87 | ) 88 | }) 89 | }), 90 | layout.Rigid(func(gtx C) D { 91 | gtx.Constraints.Max.X /= 4 92 | b := material.Button(th, &g.close, "Close") 93 | b.CornerRadius = 8 94 | return b.Layout(gtx) 95 | }), 96 | ) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /ui/gui/popups/connect.go: -------------------------------------------------------------------------------- 1 | package popups 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "gioui.org/layout" 8 | "gioui.org/widget" 9 | "gioui.org/widget/material" 10 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 11 | "github.com/bedrock-tool/bedrocktool/ui/messages" 12 | "github.com/bedrock-tool/bedrocktool/utils" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type ConnectPopup struct { 17 | g guim.Guim 18 | state string 19 | close widget.Clickable 20 | 21 | listenIP string 22 | listenPort string 23 | localIP string 24 | 25 | connectButton widget.Clickable 26 | } 27 | 28 | func NewConnect(g guim.Guim, listenAddr string) Popup { 29 | listenIp, listenPort, _ := net.SplitHostPort(listenAddr) 30 | if listenIp == "0.0.0.0" { 31 | listenIp = "127.0.0.1" 32 | } 33 | 34 | localIP, err := utils.GetLocalIP() 35 | if err != nil { 36 | logrus.Error(err) 37 | } 38 | 39 | return &ConnectPopup{ 40 | g: g, 41 | listenIP: listenIp, 42 | listenPort: listenPort, 43 | localIP: localIP, 44 | } 45 | } 46 | 47 | func (*ConnectPopup) ID() string { 48 | return "connect" 49 | } 50 | 51 | func (*ConnectPopup) Close() error { 52 | return nil 53 | } 54 | 55 | func (p *ConnectPopup) Layout(gtx C, th *material.Theme) D { 56 | if p.connectButton.Clicked(gtx) { 57 | p.g.OpenUrl(fmt.Sprintf("minecraft://connect/?serverUrl=%s&serverPort=%s", p.listenIP, p.listenPort)) 58 | } 59 | 60 | if p.close.Clicked(gtx) { 61 | p.g.ClosePopup(p.ID()) 62 | p.g.ExitSubcommand() 63 | } 64 | 65 | var connectStr string 66 | connectStr += p.listenIP 67 | if p.localIP != "" { 68 | connectStr += " or " + p.localIP 69 | } 70 | if p.listenPort != "19132" { 71 | connectStr += " with port " + p.listenPort 72 | } 73 | 74 | return LayoutPopupBackground(gtx, th, "connect", func(gtx C) D { 75 | return layout.Flex{ 76 | Axis: layout.Vertical, 77 | }.Layout(gtx, 78 | layout.Flexed(1, func(gtx C) D { 79 | return layout.Center.Layout(gtx, func(gtx C) D { 80 | return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, 81 | layout.Rigid(func(gtx C) D { 82 | switch p.state { 83 | case "listening": 84 | return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, 85 | layout.Rigid(material.Label(th, 40, "Listening").Layout), 86 | layout.Rigid(material.Body1(th, fmt.Sprintf("connect to %s", connectStr)).Layout), 87 | layout.Rigid(material.Body1(th, "in minecraft bedrock to continue").Layout), 88 | ) 89 | case "connecting-server": 90 | return material.Label(th, 40, "Connecting to Server").Layout(gtx) 91 | case "established": 92 | return material.Label(th, 40, "Established").Layout(gtx) 93 | } 94 | return D{} 95 | }), 96 | ) 97 | }) 98 | }), 99 | layout.Rigid(func(gtx C) D { 100 | gtx.Constraints.Max.X /= 2 101 | 102 | return layout.Flex{ 103 | Axis: layout.Horizontal, 104 | Spacing: layout.SpaceBetween, 105 | Alignment: layout.End, 106 | }.Layout(gtx, 107 | layout.Rigid(func(gtx C) D { 108 | b := material.Button(th, &p.close, "Close") 109 | b.CornerRadius = 8 110 | return b.Layout(gtx) 111 | }), 112 | layout.Rigid(func(gtx C) D { 113 | if p.state == "listening" { 114 | return layout.Flex{ 115 | Axis: layout.Horizontal, 116 | Alignment: layout.Middle, 117 | }.Layout(gtx, 118 | layout.Flexed(1, func(gtx C) D { 119 | b := material.Button(th, &p.connectButton, "Open Minecraft") 120 | b.CornerRadius = 8 121 | return b.Layout(gtx) 122 | }), 123 | ) 124 | } 125 | return D{} 126 | }), 127 | ) 128 | }), 129 | ) 130 | }) 131 | } 132 | 133 | func (p *ConnectPopup) HandleEvent(event any) error { 134 | switch event := event.(type) { 135 | case *messages.EventConnectStateUpdate: 136 | switch event.State { 137 | case messages.ConnectStateListening: 138 | p.state = "listening" 139 | case messages.ConnectStateServerConnecting: 140 | p.state = "connecting-server" 141 | case messages.ConnectStateEstablished: 142 | p.state = "established" 143 | case messages.ConnectStateDone: 144 | p.g.ClosePopup(p.ID()) 145 | } 146 | } 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /ui/gui/popups/errorpopup.go: -------------------------------------------------------------------------------- 1 | package popups 2 | 3 | import ( 4 | "gioui.org/layout" 5 | "gioui.org/widget" 6 | "gioui.org/widget/material" 7 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 8 | ) 9 | 10 | type errorPopup struct { 11 | g guim.Guim 12 | onClose func() 13 | err error 14 | close widget.Clickable 15 | isPanic bool 16 | } 17 | 18 | func NewErrorPopup(g guim.Guim, err error, isPanic bool, onClose func()) *errorPopup { 19 | return &errorPopup{ 20 | g: g, 21 | onClose: onClose, 22 | err: err, 23 | isPanic: isPanic, 24 | } 25 | } 26 | 27 | func (errorPopup) ID() string { 28 | return "error" 29 | } 30 | 31 | func (errorPopup) Close() error { 32 | return nil 33 | } 34 | 35 | func (e *errorPopup) Layout(gtx C, th *material.Theme) D { 36 | if e.close.Clicked(gtx) { 37 | e.g.ClosePopup(e.ID()) 38 | if e.onClose != nil { 39 | e.onClose() 40 | } 41 | return D{} 42 | } 43 | 44 | title := "Error" 45 | if e.isPanic { 46 | title = "Fatal Panic" 47 | } 48 | 49 | return LayoutPopupBackground(gtx, th, "error", func(gtx C) D { 50 | return layout.UniformInset(10).Layout(gtx, func(gtx C) D { 51 | return layout.Flex{ 52 | Axis: layout.Vertical, 53 | Alignment: layout.Start, 54 | Spacing: layout.SpaceBetween, 55 | }.Layout(gtx, 56 | layout.Rigid(material.H3(th, title).Layout), 57 | layout.Rigid(material.Body1(th, e.err.Error()).Layout), 58 | layout.Rigid(func(gtx C) D { 59 | if e.isPanic { 60 | return material.Body2(th, "More info has been printed to the console, you can submit the error to make debugging easier").Layout(gtx) 61 | } 62 | return D{} 63 | }), 64 | layout.Rigid(func(gtx C) D { 65 | return layout.Flex{ 66 | Axis: layout.Horizontal, 67 | Spacing: layout.SpaceSides, 68 | }.Layout(gtx, 69 | layout.Rigid(material.Button(th, &e.close, "Close").Layout), 70 | ) 71 | }), 72 | ) 73 | }) 74 | }) 75 | } 76 | 77 | func (e *errorPopup) HandleEvent(event any) error { 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /ui/gui/popups/popup.go: -------------------------------------------------------------------------------- 1 | package popups 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "io" 7 | 8 | "gioui.org/io/event" 9 | "gioui.org/io/pointer" 10 | "gioui.org/layout" 11 | "gioui.org/op/clip" 12 | "gioui.org/op/paint" 13 | "gioui.org/widget/material" 14 | "gioui.org/x/component" 15 | "github.com/bedrock-tool/bedrocktool/ui/messages" 16 | ) 17 | 18 | type ( 19 | C = layout.Context 20 | D = layout.Dimensions 21 | ) 22 | 23 | type Popup interface { 24 | ID() string 25 | Layout(gtx C, th *material.Theme) D 26 | io.Closer 27 | messages.EventHandler 28 | } 29 | 30 | func LayoutPopupBackground(gtx C, th *material.Theme, tag string, widget layout.Widget) D { 31 | paint.ColorOp{Color: color.NRGBA{A: 170}}.Add(gtx.Ops) 32 | paint.PaintOp{}.Add(gtx.Ops) 33 | 34 | width := gtx.Constraints.Max.X 35 | if width > gtx.Dp(300) { 36 | width -= gtx.Dp(30) 37 | } 38 | if width > gtx.Dp(600) { 39 | width = gtx.Dp(600) 40 | } 41 | //width -= gtx.Dp(unit.Dp(min(float32(width)/1000, 0.5) * 300)) 42 | return layout.Center.Layout(gtx, func(gtx C) D { 43 | defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() 44 | event.Op(gtx.Ops, tag) 45 | for { 46 | _, ok := gtx.Event(pointer.Filter{Target: tag}) 47 | if !ok { 48 | break 49 | } 50 | } 51 | 52 | component.Rect{ 53 | Color: th.Bg, 54 | Size: image.Pt(width, gtx.Dp(250)), 55 | Radii: gtx.Dp(15), 56 | }.Layout(gtx) 57 | 58 | gtx.Constraints.Min.X = width 59 | gtx.Constraints.Max.X = gtx.Constraints.Min.X 60 | gtx.Constraints.Min.Y = gtx.Dp(250) 61 | gtx.Constraints.Max.Y = gtx.Constraints.Min.Y 62 | return layout.UniformInset(8).Layout(gtx, widget) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /ui/gui/popups/realmsinput.go: -------------------------------------------------------------------------------- 1 | package popups 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "image" 7 | 8 | "gioui.org/layout" 9 | "gioui.org/widget" 10 | "gioui.org/widget/material" 11 | "gioui.org/x/component" 12 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 13 | "github.com/bedrock-tool/bedrocktool/utils" 14 | "github.com/sandertv/gophertunnel/minecraft/realms" 15 | ) 16 | 17 | type RealmsList struct { 18 | g guim.Guim 19 | setRealm func(*realms.Realm) 20 | Show widget.Bool 21 | close widget.Clickable 22 | list widget.List 23 | realms []*realmButton 24 | loaded bool 25 | loading bool 26 | } 27 | 28 | type realmButton struct { 29 | *realms.Realm 30 | widget.Clickable 31 | } 32 | 33 | func NewRealmsList(g guim.Guim, setRealm func(*realms.Realm)) Popup { 34 | return &RealmsList{ 35 | g: g, 36 | setRealm: setRealm, 37 | } 38 | } 39 | 40 | func (*RealmsList) HandleEvent(event any) error { 41 | return nil 42 | } 43 | 44 | func (*RealmsList) ID() string { 45 | return "Realms" 46 | } 47 | 48 | func (*RealmsList) Close() error { 49 | return nil 50 | } 51 | 52 | var _ Popup = &RealmsList{} 53 | 54 | func (r *RealmsList) Load() error { 55 | if !utils.Auth.LoggedIn() { 56 | return errors.New("not Logged In") 57 | } 58 | realmsList, err := utils.Auth.Realms().Realms(context.Background()) 59 | if err != nil { 60 | return err 61 | } 62 | r.realms = nil 63 | for _, realm := range realmsList { 64 | r.realms = append(r.realms, &realmButton{ 65 | Realm: &realm, 66 | }) 67 | } 68 | 69 | /* 70 | r.realms = append(r.realms, &realmButton{ 71 | Realm: &realms.Realm{ 72 | ID: 1, 73 | Name: "test", 74 | }, 75 | }) 76 | */ 77 | 78 | r.loading = false 79 | r.loaded = true 80 | return nil 81 | } 82 | 83 | func (r *RealmsList) Layout(gtx C, th *material.Theme) D { 84 | for _, realm := range r.realms { 85 | if realm.Clicked(gtx) { 86 | r.setRealm(realm.Realm) 87 | r.close.Click() 88 | } 89 | } 90 | 91 | if r.close.Clicked(gtx) { 92 | r.g.ClosePopup(r.ID()) 93 | } 94 | 95 | if !r.loaded && !r.loading { 96 | r.loading = true 97 | go func() { 98 | if !utils.Auth.LoggedIn() { 99 | <-utils.Auth.RequestLogin() 100 | } 101 | err := r.Load() 102 | if err != nil { 103 | r.g.Error(err) 104 | r.g.ClosePopup(r.ID()) 105 | } 106 | }() 107 | } 108 | 109 | return LayoutPopupBackground(gtx, th, "Realms", func(gtx C) D { 110 | return layout.Flex{ 111 | Axis: layout.Vertical, 112 | }.Layout(gtx, 113 | layout.Flexed(1, func(gtx C) D { 114 | if r.loading { 115 | return layout.Center.Layout(gtx, func(gtx C) D { 116 | gtx.Constraints.Max = image.Pt(20, 20) 117 | return material.Loader(th).Layout(gtx) 118 | }) 119 | } 120 | 121 | if len(r.realms) == 0 { 122 | return layout.Center.Layout(gtx, material.H5(th, "you have no realms").Layout) 123 | } 124 | 125 | return material.List(th, &r.list).Layout(gtx, len(r.realms), func(gtx C, index int) D { 126 | gtx.Constraints.Max.Y = min(gtx.Constraints.Max.Y, 60) 127 | realm := r.realms[index] 128 | return material.ButtonLayoutStyle{ 129 | Background: component.WithAlpha(th.ContrastBg, 0x80), 130 | Button: &realm.Clickable, 131 | CornerRadius: 8, 132 | }.Layout(gtx, func(gtx C) D { 133 | return layout.UniformInset(15).Layout(gtx, func(gtx C) D { 134 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx, 135 | layout.Rigid(material.Label(th, th.TextSize, realm.Name).Layout), 136 | ) 137 | }) 138 | }) 139 | }) 140 | }), 141 | layout.Rigid(func(gtx C) D { 142 | gtx.Constraints.Max.X /= 4 143 | b := material.Button(th, &r.close, "Close") 144 | b.CornerRadius = 8 145 | return b.Layout(gtx) 146 | }), 147 | ) 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /ui/gui/popups/update.go: -------------------------------------------------------------------------------- 1 | package popups 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gioui.org/layout" 7 | "gioui.org/unit" 8 | "gioui.org/widget" 9 | "gioui.org/widget/material" 10 | "github.com/bedrock-tool/bedrocktool/ui/gui/guim" 11 | "github.com/bedrock-tool/bedrocktool/ui/messages" 12 | "github.com/bedrock-tool/bedrocktool/utils" 13 | "github.com/bedrock-tool/bedrocktool/utils/updater" 14 | ) 15 | 16 | type UpdatePopup struct { 17 | g guim.Guim 18 | state messages.UIState 19 | startButton widget.Clickable 20 | err error 21 | updating bool 22 | } 23 | 24 | var _ Popup = &UpdatePopup{} 25 | 26 | func NewUpdatePopup(g guim.Guim) Popup { 27 | return &UpdatePopup{ 28 | g: g, 29 | state: messages.UIStateMain, 30 | } 31 | } 32 | 33 | func (p *UpdatePopup) ID() string { 34 | return "update" 35 | } 36 | 37 | func (p *UpdatePopup) Close() error { 38 | return nil 39 | } 40 | 41 | func (p *UpdatePopup) Layout(gtx C, th *material.Theme) D { 42 | if p.startButton.Clicked(gtx) && !p.updating { 43 | p.updating = true 44 | go func() { 45 | p.err = updater.DoUpdate() 46 | if p.err == nil { 47 | p.state = messages.UIStateFinished 48 | } 49 | p.updating = false 50 | p.g.ClosePopup(p.ID()) 51 | }() 52 | } 53 | 54 | update, err := updater.UpdateAvailable() 55 | if err != nil { 56 | p.err = err 57 | } 58 | 59 | return LayoutPopupBackground(gtx, th, p.ID(), func(gtx C) D { 60 | return layout.Inset{ 61 | Top: unit.Dp(25), 62 | Bottom: unit.Dp(25), 63 | Right: unit.Dp(35), 64 | Left: unit.Dp(35), 65 | }.Layout(gtx, func(gtx C) D { 66 | if p.err != nil { 67 | return layout.Center.Layout(gtx, material.H1(th, p.err.Error()).Layout) 68 | } 69 | if p.updating { 70 | return layout.Center.Layout(gtx, material.H3(th, "Updating...").Layout) 71 | } 72 | 73 | var children []layout.FlexChild 74 | switch p.state { 75 | case messages.UIStateMain: 76 | children = append(children, 77 | layout.Rigid(material.Label(th, 20, fmt.Sprintf("Current: %s\nNew: %s", utils.Version, update.Version)).Layout), 78 | layout.Rigid(material.Button(th, &p.startButton, "Do Update").Layout), 79 | ) 80 | case messages.UIStateFinished: 81 | children = append(children, 82 | layout.Rigid(material.H3(th, "Update Finished").Layout), 83 | layout.Rigid(func(gtx C) D { 84 | return layout.Center.Layout(gtx, material.Label(th, th.TextSize, "restart the app").Layout) 85 | }), 86 | ) 87 | } 88 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) 89 | }) 90 | }) 91 | } 92 | 93 | func (p *UpdatePopup) HandleEvent(event any) error { 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /ui/messages/events.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/go-gl/mathgl/mgl32" 7 | "github.com/sandertv/gophertunnel/minecraft/protocol" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type UIState uint8 12 | 13 | const ( 14 | UIStateMain UIState = iota + 1 15 | UIStateFinished 16 | ) 17 | 18 | type ConnectState uint8 19 | 20 | const ( 21 | ConnectStateBegin ConnectState = iota + 1 22 | ConnectStateListening 23 | ConnectStateServerConnecting 24 | ConnectStateReceivingResources 25 | ConnectStateEstablished 26 | ConnectStateDone 27 | ) 28 | 29 | type EventSetValue struct { 30 | Name string 31 | Value string 32 | } 33 | 34 | type EventSetUIState struct { 35 | State UIState 36 | } 37 | 38 | type EventConnectStateUpdate struct { 39 | State ConnectState 40 | ListenAddr string 41 | } 42 | 43 | type EventUpdateAvailable struct { 44 | Version string 45 | } 46 | 47 | type EventUpdateDownloadProgress struct { 48 | Progress int 49 | } 50 | 51 | type EventUpdateDoInstall struct { 52 | Filepath string 53 | } 54 | 55 | type EventError struct { 56 | Error error 57 | } 58 | 59 | // 60 | 61 | type EventProcessingWorldUpdate struct { 62 | WorldName string 63 | State string 64 | } 65 | 66 | type EventFinishedSavingWorld struct { 67 | WorldName string 68 | Filepath string 69 | Chunks int 70 | Entities int 71 | } 72 | 73 | // 74 | 75 | type EventInitialPacksInfo struct { 76 | Packs []protocol.TexturePackInfo 77 | KeysOnly bool 78 | } 79 | 80 | type EventProcessingPack struct { 81 | ID string 82 | } 83 | 84 | type EventPackDownloadProgress struct { 85 | ID string 86 | BytesAdded int 87 | } 88 | 89 | type EventFinishedPack struct { 90 | ID string 91 | Name string 92 | Version string 93 | Filepath string 94 | Icon *image.RGBA 95 | Error error 96 | } 97 | 98 | type EventDisplayAuthCode struct { 99 | AuthCode string 100 | URI string 101 | } 102 | 103 | type EventAuthFinished struct { 104 | Error error 105 | } 106 | 107 | type MapTile struct { 108 | Pos protocol.ChunkPos 109 | Img image.RGBA 110 | } 111 | 112 | type EventMapTiles struct { 113 | Tiles []MapTile 114 | } 115 | 116 | type EventResetMap struct{} 117 | 118 | type EventPlayerPosition struct { 119 | Position mgl32.Vec3 120 | } 121 | 122 | type EventPlayerSkin struct { 123 | PlayerName string 124 | Skin protocol.Skin 125 | } 126 | 127 | // 128 | 129 | type EventHandler interface { 130 | HandleEvent(event any) error 131 | } 132 | 133 | var eventHandler func(event any) error 134 | 135 | func SetEventHandler(f func(event any) error) { 136 | eventHandler = f 137 | } 138 | 139 | func SendEvent(event any) { 140 | //fmt.Printf("event %s\n", reflect.TypeOf(event).String()) 141 | err := eventHandler(event) 142 | if err != nil { 143 | logrus.Errorf("event handler errored %s", err) 144 | } 145 | } 146 | 147 | type AuthHandler struct{} 148 | 149 | func (a *AuthHandler) AuthCode(uri, code string) { 150 | SendEvent(&EventDisplayAuthCode{ 151 | URI: uri, 152 | AuthCode: code, 153 | }) 154 | } 155 | 156 | func (a *AuthHandler) Finished(err error) { 157 | SendEvent(&EventAuthFinished{ 158 | Error: err, 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type UI interface { 8 | Init() error 9 | Start(context.Context, context.CancelCauseFunc) error 10 | } 11 | -------------------------------------------------------------------------------- /utils/behaviourpack/biomes.go: -------------------------------------------------------------------------------- 1 | package behaviourpack 2 | 3 | import "github.com/sandertv/gophertunnel/minecraft/protocol" 4 | 5 | type biomeBehaviour struct { 6 | FormatVersion string `json:"format_version"` 7 | MinecraftBiome MinecraftBiome `json:"minecraft:biome"` 8 | } 9 | 10 | type biomeDescription struct { 11 | Identifier string `json:"identifier"` 12 | } 13 | 14 | type MinecraftBiome struct { 15 | Description biomeDescription `json:"description"` 16 | } 17 | 18 | func (b *Pack) AddBiome(biomeName string, definition protocol.BiomeDefinition) { 19 | _ = definition 20 | b.biomes = append(b.biomes, biomeBehaviour{ 21 | FormatVersion: "1.13.0", 22 | MinecraftBiome: MinecraftBiome{ 23 | Description: biomeDescription{ 24 | Identifier: biomeName, 25 | }, 26 | }, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /utils/behaviourpack/block.go: -------------------------------------------------------------------------------- 1 | package behaviourpack 2 | 3 | import ( 4 | "github.com/sandertv/gophertunnel/minecraft/protocol" 5 | ) 6 | 7 | type BlockBehaviour struct { 8 | FormatVersion string `json:"format_version"` 9 | MinecraftBlock MinecraftBlock `json:"minecraft:block"` 10 | } 11 | 12 | func (bp *Pack) AddBlock(block protocol.BlockEntry) { 13 | ns, _ := splitNamespace(block.Name) 14 | if ns == "minecraft" { 15 | return 16 | } 17 | 18 | minecraftBlock, version := parseBlock(block) 19 | 20 | bp.blocks[block.Name] = &BlockBehaviour{ 21 | FormatVersion: version, 22 | MinecraftBlock: minecraftBlock, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utils/behaviourpack/bp.go: -------------------------------------------------------------------------------- 1 | package behaviourpack 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "path" 7 | "strings" 8 | 9 | "github.com/bedrock-tool/bedrocktool/utils" 10 | "github.com/google/uuid" 11 | "github.com/sandertv/gophertunnel/minecraft/resource" 12 | ) 13 | 14 | type Pack struct { 15 | formatVersion string 16 | Manifest *resource.Manifest 17 | blocks map[string]*BlockBehaviour 18 | items map[string]*itemBehaviour 19 | entities map[string]*entityBehaviour 20 | biomes []biomeBehaviour 21 | } 22 | 23 | func New(name string) *Pack { 24 | return &Pack{ 25 | formatVersion: "1.16.0", 26 | Manifest: &resource.Manifest{ 27 | FormatVersion: 2, 28 | Header: resource.Header{ 29 | Name: name, 30 | Description: "Adds Blocks, Items and Entities from the server to this world", 31 | UUID: uuid.MustParse(utils.RandSeededUUID(name + "_datapack")), 32 | Version: resource.Version{1, 0, 0}, 33 | MinimumGameVersion: resource.Version{1, 19, 50}, 34 | }, 35 | Modules: []resource.Module{ 36 | { 37 | Type: "data", 38 | UUID: utils.RandSeededUUID(name + "_data_module"), 39 | Description: "Datapack", 40 | Version: resource.Version{1, 0, 0}, 41 | }, 42 | }, 43 | Dependencies: []resource.Dependency{}, 44 | Capabilities: []resource.Capability{}, 45 | }, 46 | blocks: make(map[string]*BlockBehaviour), 47 | items: make(map[string]*itemBehaviour), 48 | entities: make(map[string]*entityBehaviour), 49 | } 50 | } 51 | 52 | func (bp *Pack) AddDependency(id string, ver resource.Version) { 53 | bp.Manifest.Dependencies = append(bp.Manifest.Dependencies, resource.Dependency{ 54 | UUID: id, 55 | Version: ver, 56 | }) 57 | } 58 | 59 | func fsFileExists(f fs.FS, name string) bool { 60 | file, err := f.Open(name) 61 | if err != nil { 62 | return false 63 | } 64 | file.Close() 65 | return true 66 | } 67 | 68 | func (bp *Pack) CheckAddLink(pack resource.Pack) { 69 | hasBlocksJson := bp.HasBlocks() && fsFileExists(pack, "blocks.json") 70 | hasEntitiesFolder := bp.HasEntities() && fsFileExists(pack, "entity") 71 | hasItemsFolder := bp.HasItems() && fsFileExists(pack, "items") 72 | 73 | // has no assets needed 74 | if !(hasBlocksJson || hasEntitiesFolder || hasItemsFolder) { 75 | return 76 | } 77 | 78 | h := pack.Manifest().Header 79 | bp.AddDependency(h.UUID.String(), h.Version) 80 | } 81 | 82 | func (bp *Pack) HasBlocks() bool { 83 | return len(bp.blocks) > 0 84 | } 85 | 86 | func (bp *Pack) HasItems() bool { 87 | return len(bp.items) > 0 88 | } 89 | 90 | func (bp *Pack) HasEntities() bool { 91 | return len(bp.entities) > 0 92 | } 93 | 94 | func (bp *Pack) HasContent() bool { 95 | return bp.HasBlocks() || bp.HasItems() 96 | } 97 | 98 | func splitNamespace(identifier string) (ns, name string) { 99 | split := strings.SplitN(identifier, ":", 2) 100 | return split[0], split[len(split)-1] 101 | } 102 | 103 | func (bp *Pack) Save(fs utils.WriterFS) error { 104 | if err := utils.WriteManifest(bp.Manifest, fs, ""); err != nil { 105 | return err 106 | } 107 | 108 | _add_thing := func(base, identifier string, thing any) error { 109 | ns, name := splitNamespace(identifier) 110 | dir := path.Join(base, ns) 111 | w, err := fs.Create(path.Join(dir, name+".json")) 112 | if err != nil { 113 | return err 114 | } 115 | defer w.Close() 116 | e := json.NewEncoder(w) 117 | e.SetIndent("", "\t") 118 | return e.Encode(thing) 119 | } 120 | 121 | for k := range bp.items { 122 | _, ok := bp.blocks[k] 123 | if ok { 124 | delete(bp.items, k) 125 | } 126 | } 127 | 128 | if bp.HasBlocks() { // blocks 129 | for _, be := range bp.blocks { 130 | err := _add_thing("blocks", be.MinecraftBlock.Description.Identifier, be) 131 | if err != nil { 132 | return err 133 | } 134 | } 135 | } 136 | if bp.HasItems() { // items 137 | for _, ib := range bp.items { 138 | err := _add_thing("items", ib.MinecraftItem.Description.Identifier, ib) 139 | if err != nil { 140 | return err 141 | } 142 | } 143 | } 144 | if bp.HasEntities() { // entities 145 | for _, eb := range bp.entities { 146 | err := _add_thing("entities", eb.MinecraftEntity.Description.Identifier, eb) 147 | if err != nil { 148 | return err 149 | } 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /utils/behaviourpack/item.go: -------------------------------------------------------------------------------- 1 | package behaviourpack 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bedrock-tool/bedrocktool/utils" 7 | "github.com/sandertv/gophertunnel/minecraft/protocol" 8 | ) 9 | 10 | type itemDescription struct { 11 | Category string `json:"category"` 12 | Identifier string `json:"identifier"` 13 | IsExperimental bool `json:"is_experimental"` 14 | } 15 | 16 | type minecraftItem struct { 17 | Description itemDescription `json:"description"` 18 | Components map[string]any `json:"components,omitempty"` 19 | } 20 | 21 | type itemBehaviour struct { 22 | FormatVersion string `json:"format_version"` 23 | MinecraftItem minecraftItem `json:"minecraft:item"` 24 | } 25 | 26 | func (bp *Pack) AddItem(item protocol.ItemEntry) { 27 | ns, _ := splitNamespace(item.Name) 28 | if ns == "minecraft" { 29 | return 30 | } 31 | 32 | bp.items[item.Name] = &itemBehaviour{ 33 | FormatVersion: "1.20.50", 34 | MinecraftItem: minecraftItem{ 35 | Description: itemDescription{ 36 | Identifier: item.Name, 37 | IsExperimental: true, 38 | }, 39 | Components: make(map[string]any), 40 | }, 41 | } 42 | } 43 | 44 | func processItemComponent(name string, component map[string]any, componentsOut map[string]any) (string, any) { 45 | switch name { 46 | case "item_properties": 47 | if icon, ok := component["minecraft:icon"].(map[string]any); ok { 48 | if textures, ok := icon["textures"].(map[string]any); ok { 49 | componentsOut["minecraft:icon"] = map[string]any{ 50 | "texture": textures["default"], 51 | } 52 | } 53 | } 54 | return name, component 55 | 56 | case "minecraft:icon": 57 | if textures, ok := component["textures"].(map[string]any); ok { 58 | return name, map[string]any{ 59 | "texture": textures["default"], 60 | } 61 | } 62 | return "", nil 63 | 64 | case "minecraft:interact_button": 65 | return name, component["interact_text"] 66 | 67 | case "item_tags": 68 | return "", nil 69 | 70 | case "minecraft:durability": 71 | return name, component 72 | 73 | default: 74 | if utils.IsDebug() { 75 | fmt.Printf("unhandled component %s\n%v\n\n", name, component) 76 | } 77 | return name, component 78 | } 79 | } 80 | 81 | func (bp *Pack) ApplyComponentEntries(entries []protocol.ItemEntry) { 82 | for _, ice := range entries { 83 | item, ok := bp.items[ice.Name] 84 | if !ok { 85 | continue 86 | } 87 | if components, ok := ice.Data["components"].(map[string]any); ok { 88 | var componentsOut = make(map[string]any) 89 | for name, component := range components { 90 | componentMap, ok := component.(map[string]any) 91 | if !ok { 92 | fmt.Printf("skipped component %s %v\n", name, component) 93 | continue 94 | } 95 | nameOut, value := processItemComponent(name, componentMap, componentsOut) 96 | if name == "" { 97 | continue 98 | } 99 | componentsOut[nameOut] = value 100 | } 101 | item.MinecraftItem.Components = componentsOut 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /utils/cfb8_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | type nullReader struct{} 11 | 12 | func (nullReader) Read(b []byte) (int, error) { 13 | for i := range b { 14 | b[i] = 0 15 | } 16 | return len(b), nil 17 | } 18 | 19 | func Benchmark_cfb8(b *testing.B) { 20 | cfb8 := NewCfb8(nullReader{}, make([]byte, 32)) 21 | var buf = make([]byte, 20000) 22 | b.SetBytes(int64(len(buf))) 23 | 24 | for b.Loop() { 25 | cfb8.Read(buf) 26 | } 27 | } 28 | 29 | func Benchmark_oldCfb8(b *testing.B) { 30 | cfb8 := NewOldCfb8(nullReader{}, make([]byte, 32)) 31 | var buf = make([]byte, 128000) 32 | 33 | for i := 0; i < b.N; i++ { 34 | cfb8.Read(buf) 35 | } 36 | } 37 | 38 | type oldCfb8 struct { 39 | r io.Reader 40 | cipher cipher.Block 41 | shiftRegister []byte 42 | iv []byte 43 | } 44 | 45 | func NewOldCfb8(r io.Reader, key []byte) io.Reader { 46 | c := &oldCfb8{ 47 | r: r, 48 | } 49 | c.cipher, _ = aes.NewCipher(key) 50 | c.shiftRegister = make([]byte, 16) 51 | copy(c.shiftRegister, key[:16]) 52 | c.iv = make([]byte, 16) 53 | return c 54 | } 55 | 56 | func (c *oldCfb8) Read(dst []byte) (n int, err error) { 57 | n, err = c.r.Read(dst) 58 | if n > 0 { 59 | c.shiftRegister = append(c.shiftRegister, dst[:n]...) 60 | for off := 0; off < n; off += 1 { 61 | c.cipher.Encrypt(c.iv, c.shiftRegister) 62 | dst[off] ^= c.iv[0] 63 | c.shiftRegister = c.shiftRegister[1:] 64 | } 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /utils/chunk_render.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/df-mc/dragonfly/server/block" 8 | "github.com/df-mc/dragonfly/server/block/cube" 9 | "github.com/df-mc/dragonfly/server/world" 10 | "github.com/df-mc/dragonfly/server/world/chunk" 11 | "github.com/sandertv/gophertunnel/minecraft/protocol" 12 | "github.com/sandertv/gophertunnel/minecraft/resource" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func isBlockLightblocking(b world.Block) bool { 17 | d, isDiffuser := b.(block.LightDiffuser) 18 | noDiffuse := isDiffuser && d.LightDiffusionLevel() == 0 19 | return !noDiffuse 20 | } 21 | 22 | var waterColor = block.Water{}.Color() 23 | var notFoundColor = color.RGBA{0xff, 0, 0xff, 0xff} 24 | 25 | func (cr *ChunkRenderer) blockColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) (blockColor color.RGBA) { 26 | if y <= int16(c.Range().Min()) { 27 | return color.RGBA{0, 0, 0, 0} 28 | } 29 | rid := c.Block(x, y, z, 0) 30 | 31 | br := c.BlockRegistry.(world.BlockRegistry) 32 | b, found := br.BlockByRuntimeID(rid) 33 | if !found { 34 | return notFoundColor 35 | } 36 | 37 | if _, isWater := b.(block.Water); isWater { 38 | // get the first non water block at the position 39 | heightBlock := c.HeightMap().At(x, z) 40 | depth := y - heightBlock 41 | if depth > 0 { 42 | blockColor = cr.blockColorAt(c, x, heightBlock, z) 43 | } else { 44 | blockColor = color.RGBA{0, 0, 0, 0} 45 | } 46 | 47 | // blend that blocks color with water depending on depth 48 | waterColor.A = uint8(min(150+depth*7, 230)) 49 | blockColor = BlendColors(blockColor, waterColor) 50 | blockColor.R -= uint8(depth * 6) 51 | blockColor.G -= uint8(depth * 6) 52 | blockColor.B -= uint8(depth * 6) 53 | return blockColor 54 | } 55 | 56 | if b2, ok := b.(world.UnknownBlock); ok { 57 | name, _ := b2.EncodeBlock() 58 | customColor, ok := cr.customBlockColors[name] 59 | if ok { 60 | blockColor = customColor 61 | goto haveColor 62 | } 63 | 64 | blockColor = LookupColor(name) 65 | goto haveColor 66 | } else { 67 | blockColor = b.Color() 68 | } 69 | 70 | haveColor: 71 | if blockColor.R == 0xff && blockColor.G == 0x0 && blockColor.B == 0xff { 72 | if IsDebug() { 73 | name, props := b.EncodeBlock() 74 | logrus.Infof("no color %s %v", name, props) 75 | b.Color() 76 | } 77 | } 78 | 79 | if blockColor.A != 0xff { 80 | blockColor = BlendColors(cr.blockColorAt(c, x, y-1, z), blockColor) 81 | } 82 | return blockColor 83 | } 84 | 85 | func (cr *ChunkRenderer) chunkGetColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) color.RGBA { 86 | br := c.BlockRegistry.(world.BlockRegistry) 87 | haveUp := false 88 | cube.Pos{int(x), int(y), int(z)}. 89 | Side(cube.FaceUp). 90 | Neighbours(func(neighbour cube.Pos) { 91 | if neighbour.X() < 0 || neighbour.X() >= 16 || neighbour.Z() < 0 || neighbour.Z() >= 16 || neighbour.Y() > c.Range().Max() || haveUp { 92 | return 93 | } 94 | blockRid := c.Block(uint8(neighbour[0]), int16(neighbour[1]), uint8(neighbour[2]), 0) 95 | if blockRid > 0 { 96 | b, found := br.BlockByRuntimeID(blockRid) 97 | if found { 98 | if isBlockLightblocking(b) { 99 | haveUp = true 100 | } 101 | } 102 | } 103 | }, cube.Range{int(y + 1), int(y + 1)}) 104 | 105 | blockColor := cr.blockColorAt(c, x, y, z) 106 | if haveUp && (x+z)%2 == 0 { 107 | if blockColor.R > 10 { 108 | blockColor.R -= 10 109 | } 110 | if blockColor.G > 10 { 111 | blockColor.G -= 10 112 | } 113 | if blockColor.B > 10 { 114 | blockColor.B -= 10 115 | } 116 | } 117 | return blockColor 118 | } 119 | 120 | type ChunkRenderer struct { 121 | customBlockColors map[string]color.RGBA 122 | } 123 | 124 | func (cr *ChunkRenderer) ResolveColors(entries []protocol.BlockEntry, packs []resource.Pack) { 125 | colors := ResolveColors(entries, packs) 126 | cr.customBlockColors = colors 127 | } 128 | 129 | func (cr *ChunkRenderer) Chunk2Img(c *chunk.Chunk) *image.RGBA { 130 | img := image.NewRGBA(image.Rect(0, 0, 16, 16)) 131 | hm := c.HeightMapWithWater() 132 | 133 | for x := uint8(0); x < 16; x++ { 134 | for z := uint8(0); z < 16; z++ { 135 | img.SetRGBA( 136 | int(x), int(z), 137 | cr.chunkGetColorAt(c, x, hm.At(x, z), z), 138 | ) 139 | } 140 | } 141 | return img 142 | } 143 | -------------------------------------------------------------------------------- /utils/commands/register.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var Registered = map[string]Command{} 8 | 9 | type Command interface { 10 | Name() string 11 | Description() string 12 | Settings() any 13 | Run(ctx context.Context, settings any) error 14 | } 15 | 16 | func RegisterCommand(sub Command) { 17 | Registered[sub.Name()] = sub 18 | } 19 | -------------------------------------------------------------------------------- /utils/connect_info.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "path" 9 | "strings" 10 | 11 | "github.com/bedrock-tool/bedrocktool/utils/discovery" 12 | "github.com/sandertv/gophertunnel/minecraft/realms" 13 | ) 14 | 15 | type ConnectInfo struct { 16 | Value string 17 | 18 | gathering *discovery.Gathering 19 | realm *realms.Realm 20 | } 21 | 22 | func (c *ConnectInfo) getGathering(ctx context.Context, name string) (*discovery.Gathering, error) { 23 | if c.gathering != nil && c.gathering.Title == name { 24 | return c.gathering, nil 25 | } 26 | gatheringsService, err := Auth.Gatherings(ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | gatherings, err := gatheringsService.Gatherings(ctx) 31 | if err != nil { 32 | return nil, err 33 | } 34 | for _, gathering := range gatherings { 35 | title := strings.ToLower(gathering.Title) 36 | id := strings.ToLower(gathering.GatheringID) 37 | if strings.HasPrefix(title, name) || strings.HasPrefix(id, name) { 38 | return gathering, nil 39 | } 40 | } 41 | return nil, fmt.Errorf("gathering %s not found", name) 42 | } 43 | 44 | func (c *ConnectInfo) getRealm(ctx context.Context, name string) (*realms.Realm, error) { 45 | if c.realm != nil && c.realm.Name == name { 46 | return c.realm, nil 47 | } 48 | realms, err := Auth.Realms().Realms(ctx) 49 | if err != nil { 50 | return nil, err 51 | } 52 | for _, realm := range realms { 53 | if strings.HasPrefix(strings.ToLower(realm.Name), strings.ToLower(name)) { 54 | return &realm, nil 55 | } 56 | } 57 | return nil, fmt.Errorf("realm %s not found", name) 58 | } 59 | 60 | func (c *ConnectInfo) Name(ctx context.Context) (string, error) { 61 | info, err := parseConnectInfo(c.Value) 62 | if err != nil { 63 | return "", nil 64 | } 65 | if info.serverAddress != "" { 66 | host, port, err := net.SplitHostPort(info.serverAddress) 67 | if err != nil { 68 | host = info.serverAddress 69 | } else if port != "19132" { 70 | host += "_" + port 71 | } 72 | 73 | return host, nil 74 | } 75 | if info.replayName != "" { 76 | return path.Base(info.replayName), nil 77 | } 78 | if info.realmName != "" { 79 | realm, err := c.getRealm(ctx, info.realmName) 80 | if err != nil { 81 | return "", err 82 | } 83 | return realm.Name, nil 84 | } 85 | if info.gatheringName != "" { 86 | gathering, err := c.getGathering(ctx, info.gatheringName) 87 | if err != nil { 88 | return "", err 89 | } 90 | return gathering.Title, nil 91 | } 92 | return "invalid", nil 93 | } 94 | 95 | func (c *ConnectInfo) Address(ctx context.Context) (string, error) { 96 | info, err := parseConnectInfo(c.Value) 97 | if err != nil { 98 | return "", err 99 | } 100 | if info.serverAddress != "" { 101 | return info.serverAddress, nil 102 | } 103 | if info.replayName != "" { 104 | return info.replayName, nil 105 | } 106 | if info.realmName != "" { 107 | realm, err := c.getRealm(ctx, info.realmName) 108 | if err != nil { 109 | return "", err 110 | } 111 | return realm.Address(ctx) 112 | } 113 | if info.gatheringName != "" { 114 | gathering, err := c.getGathering(ctx, info.gatheringName) 115 | if err != nil { 116 | return "", err 117 | } 118 | return gathering.Address(ctx) 119 | } 120 | return "", errors.New("invalid address") 121 | } 122 | 123 | func (c *ConnectInfo) IsReplay() bool { 124 | return pcapRegex.MatchString(c.Value) 125 | } 126 | 127 | func (c *ConnectInfo) SetRealm(realm *realms.Realm) { 128 | c.Value = "realm:" + realm.Name 129 | c.realm = realm 130 | } 131 | 132 | func (c *ConnectInfo) SetGathering(gathering *discovery.Gathering) { 133 | c.Value = "gathering:" + gathering.Title 134 | c.gathering = gathering 135 | } 136 | 137 | type parsedConnectInfo struct { 138 | gatheringName string 139 | realmName string 140 | replayName string 141 | serverAddress string 142 | } 143 | 144 | func parseConnectInfo(value string) (*parsedConnectInfo, error) { 145 | if gatheringRegex.MatchString(value) { 146 | p := regexGetParams(gatheringRegex, value) 147 | input := strings.ToLower(p["Title"]) 148 | return &parsedConnectInfo{gatheringName: input}, nil 149 | } 150 | 151 | // realm 152 | if realmRegex.MatchString(value) { 153 | p := regexGetParams(realmRegex, value) 154 | input := strings.ToLower(p["Name"]) 155 | return &parsedConnectInfo{realmName: input}, nil 156 | } 157 | 158 | // pcap replay 159 | if pcapRegex.MatchString(value) { 160 | p := regexGetParams(pcapRegex, value) 161 | input := p["Filename"] 162 | return &parsedConnectInfo{replayName: input}, nil 163 | } 164 | 165 | // normal server dns or ip 166 | serverAddress := value 167 | if len(strings.Split(serverAddress, ":")) == 1 { 168 | serverAddress += ":19132" 169 | } 170 | return &parsedConnectInfo{serverAddress: serverAddress}, nil 171 | } 172 | -------------------------------------------------------------------------------- /utils/connect_info_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestParseConnectInfo(t *testing.T) { 6 | type test struct { 7 | value string 8 | expected parsedConnectInfo 9 | } 10 | var tests = []test{ 11 | {value: "minecraft.net", expected: parsedConnectInfo{serverAddress: "minecraft.net:19132"}}, 12 | {value: "realm:test-realm", expected: parsedConnectInfo{realmName: "test-realm"}}, 13 | {value: "gathering:test-gathering", expected: parsedConnectInfo{gatheringName: "test-gathering"}}, 14 | {value: "test-capture.pcap2", expected: parsedConnectInfo{replayName: "test-capture.pcap2"}}, 15 | } 16 | 17 | for _, tt := range tests { 18 | r, err := parseConnectInfo(tt.value) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if *r != tt.expected { 23 | t.Fatalf("%s expected: %v\ngot: %v\n", tt.value, tt.expected, r) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /utils/crypt/crypt.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | _ "embed" 7 | "io" 8 | "os" 9 | "time" 10 | 11 | "golang.org/x/crypto/openpgp" 12 | "golang.org/x/crypto/openpgp/armor" 13 | "golang.org/x/crypto/openpgp/packet" 14 | ) 15 | 16 | //go:embed key.gpg 17 | var key_gpg []byte 18 | var recipients []*openpgp.Entity 19 | 20 | func init() { 21 | block, err := armor.Decode(bytes.NewBuffer(key_gpg)) 22 | if err != nil { 23 | panic(err) 24 | } 25 | recip, err := openpgp.ReadEntity(packet.NewReader(block.Body)) 26 | if err != nil { 27 | panic(err) 28 | } 29 | recipients = append(recipients, recip) 30 | } 31 | 32 | func Enc(name string, data []byte) ([]byte, error) { 33 | w := bytes.NewBuffer(nil) 34 | wc, err := openpgp.Encrypt(w, recipients, nil, &openpgp.FileHints{ 35 | IsBinary: true, FileName: name, ModTime: time.Now(), 36 | }, nil) 37 | if err != nil { 38 | return nil, err 39 | } 40 | if _, err = wc.Write(data); err != nil { 41 | return nil, err 42 | } 43 | wc.Close() 44 | return w.Bytes(), nil 45 | } 46 | 47 | func Encer(filename string) (io.WriteCloser, func() error, error) { 48 | w, err := os.Create(filename) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | bw := bufio.NewWriter(w) 53 | wc, err := openpgp.Encrypt(bw, recipients, nil, &openpgp.FileHints{ 54 | IsBinary: true, FileName: filename, ModTime: time.Now(), 55 | }, nil) 56 | return wc, bw.Flush, err 57 | } 58 | -------------------------------------------------------------------------------- /utils/crypt/key.gpg: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQGNBGHqFb8BDADdhaCCihGEuMNtgHo931t2/H6D6I/j+kYZkgt54XeQqTQVLkaW 4 | FzL/MkLB5Hu3CQl1BlCdg8wRyOLdLOyhwtiAiFGcmUel28eIM+Y/Hcr+cUErGrEd 5 | M3r2VjCLytgQeX+jip/Wu/xVrizsCwuy4oxJN1DtAuUMNjP78TBdIa9MJfc68NTW 6 | YAbvG6XT3CJaTHQwWGMUC500Jtg647aRMCdWS1JXQqZiIdqFy/dFCgpfRycg9vpz 7 | RbxQ+EDZ23GDljIcDeaCJ6WWBMIw2pSdTA6psGl4FMeGfHGWaMPudpm5AATG/X1W 8 | bKzhs4JEDcXwu6Si65j7I6BiaxuARfBgRnLBOPhAt+TE0K6jzUcIQHMeLBWk1rfw 9 | wP7hK76OGQiLY4BvpMWyuNXGHy3Le1uePknNBVZppusSgfvm7TUytBg7RfWvw2Dr 10 | j+N07p6KbAn6NelewsybCyeh0k94FvaAtjodjDo1sIogiqLYWleAH/IAF2jFREq6 11 | oGbix9d5UBX+SNEAEQEAAbQdb2xlYmVjayA8b2xlYmVja0BvbGViZWNrLmNvbT6J 12 | Ac4EEwEIADgWIQSGV2qyDgjKdVHooj7U/i4We6MHnQUCYeoVvwIbAwULCQgHAgYV 13 | CgkICwIEFgIDAQIeAQIXgAAKCRDU/i4We6MHnbEzDACrA05J9HvBiQSNXRuVcgT3 14 | 5I5brahyhtGdGbbmanvYBNdABkmXsXcVErF4hYASIkQfSZ0uui0kOnmsQRjoKuIX 15 | +agp5d9/S67gwdkafPjkj/vBtCmpFdoNoe24njvlNY0Wd6dYhE0jqCk95ZX0E+AR 16 | J3L1t+f9uFB5GfyVU9oBpSXqZsJD4AoDa7nPz5vNVm3cxPKivlXv1Q5HV96Ngk8R 17 | 6Y+hIk8vF5YJ5Q7HLf4xAzqHgbNo8IkbsPAg1uiLozo6bQD0vh8ash9bBycmWujl 18 | jKpDitqPRjNsOhXQ322v90QR2s9mHRyJdi4duSXWPCKlsoTNL4o5Kd/AX/qR1hB8 19 | 4Ml7rTI0LDUI5vf2K9p2lxD45ZwJyI8VrORvzjdQcddvtJge6MVQyRktrzEkSeuc 20 | sAjW2xcswgHChmP8f56gLUTZZmAk5TK3A61UkJW8oU0qNBRl4j+Yd84vonW2Dlm2 21 | V20cNmBg7qg2Uldn0TAQrKtzLVQsRp46pFOP6RWcMbO5AY0EYeoVvwEMALo7jQBr 22 | Jtjr2C/abVAmI/ToEif3MPcFv/LkBLuEttTTkDJw9fdj3y/iuqy5EEyG07J8t/+z 23 | PjDLgLUuyNCpxobi80b7GgukDTeiw94ezTyIIXxtb7aYBlHZ/uDecJzNukZ4yNIx 24 | mX/YrUx4isV1APqa66y+eH0aGBZQIZ1sOPeGgsPOVZmIjFDPWEPqBBCQLD96M/Kd 25 | FTRYM8SslnLxBfeX7iye4ZVmLgGyWrJq4cG88Rj++cnp4XI25pJoNgE4GHoQYuep 26 | XazwU8PaJgudQyynIDCrsgDEiCukAY7ZoiVmLSmxWVNNNAdXYKWW88XUMZ0raSSB 27 | H9AsPK4c2YSd916E93nbL3TSiYkVt3Ahty9VAwThoYHZ4Z8/ddD5t6HRTR+/tZn4 28 | JLMskr2FuCb9lR+jOGCp3jW7UezX6G3SLJFB/afBqjhPhurm8Dz63psxIsUZPMzb 29 | yzDXXMjo5lfe1yNXs/joHq4ni74ASpSheO6Kigj+N29hQEZ5AvgkBke/vwARAQAB 30 | iQG2BBgBCAAgFiEEhldqsg4IynVR6KI+1P4uFnujB50FAmHqFb8CGwwACgkQ1P4u 31 | FnujB52j3gwAyO7tBmpNy2NF+LumtUhsB8QYKAs2Xwo7WNQMdYkKrFMD3umXI6n2 32 | BpnfpoJWKeA9HOwJZwdaEggvzcZw2/KPLOW0L2XEOLMoDsJMLQkfaw9ewG9A//em 33 | E0RzTXP1vdbIVdjbNsNmfGa5MiniNDt0khiOkC6u/IXu767vTrVxQwwBvbj/Jhjz 34 | amCuwdFDl4SsadsCm8amYKRFi9k9j2jkYRJSy3KomG7b+2ZfUbmdJoL+NnIivVFZ 35 | AN6WpWhC0Usxgm5xjLLi5f0DlnkOIdPiq8oajA7iCuCGoJvYMZddGigdRdJCZmaR 36 | h19ELdyh/t21ySGCOckkDNQ4cGcm4mm8hilHkJDNsTJ/ACQFjzNyg9mwZ+80fjFa 37 | Wnl6U3bkPsUJsI5vfgVb2td51mxEe6DYd5MTDiKkchlbO+J3vQ0FOb6xkuVSJp0W 38 | ZNl7HmnETLuKjemNoW8Gj0IB0AipLwrisORbYpee1mN2YDasr+0cK5ADIuBXvx6f 39 | cNMCuTrO1l7m 40 | =AZpZ 41 | -----END PGP PUBLIC KEY BLOCK----- 42 | -------------------------------------------------------------------------------- /utils/discovery/authservice.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strings" 11 | "time" 12 | 13 | "github.com/google/uuid" 14 | "github.com/sandertv/gophertunnel/minecraft/protocol" 15 | ) 16 | 17 | type AuthService struct { 18 | ServiceWithEdu 19 | Issuer string `json:"issuer"` 20 | } 21 | 22 | type mcTokenDevice struct { 23 | ApplicationType string `json:"applicationType"` 24 | Capabilities []string `json:"capabilities"` 25 | GameVersion string `json:"gameVersion"` 26 | ID string `json:"id"` 27 | Memory string `json:"memory"` 28 | Platform string `json:"platform"` 29 | PlayFabTitleID string `json:"playFabTitleId"` 30 | StorePlatform string `json:"storePlatform"` 31 | TreatmentOverrides any `json:"treatmentOverrides"` 32 | Type string `json:"type"` 33 | } 34 | 35 | type mcTokenUser struct { 36 | Language string `json:"language"` 37 | LanguageCode string `json:"languageCode"` 38 | RegionCode string `json:"regionCode"` 39 | Token string `json:"token"` 40 | TokenType string `json:"tokenType"` 41 | } 42 | type mcTokenRequest struct { 43 | Device mcTokenDevice `json:"device"` 44 | User mcTokenUser `json:"user"` 45 | } 46 | 47 | type MCToken struct { 48 | AuthorizationHeader string `json:"authorizationHeader"` 49 | ValidUntil time.Time `json:"validUntil"` 50 | Treatments []string `json:"treatments"` 51 | Configurations struct { 52 | Minecraft struct { 53 | ID string `json:"id"` 54 | Parameters map[string]any `json:"parameters"` 55 | } `json:"minecraft"` 56 | } `json:"configurations"` 57 | } 58 | 59 | type mcTokenResponse struct { 60 | Result MCToken `json:"result"` 61 | } 62 | 63 | func (a *AuthService) StartSession(ctx context.Context, xblToken, titleid string) (*MCToken, error) { 64 | resp, err := doRequest[mcTokenResponse](ctx, http.DefaultClient, "POST", fmt.Sprintf("%s/api/v1.0/session/start", a.ServiceURI), mcTokenRequest{ 65 | Device: mcTokenDevice{ 66 | ApplicationType: "MinecraftPE", 67 | Capabilities: []string{"RayTracing"}, 68 | GameVersion: protocol.CurrentVersion, 69 | ID: uuid.New().String(), 70 | Memory: fmt.Sprintf("%d", int64(16*(1024*1024*1024))), // 16 GB 71 | Platform: "Windows10", 72 | PlayFabTitleID: strings.ToUpper(a.PlayfabTitleID), 73 | StorePlatform: "uwp.store", 74 | TreatmentOverrides: nil, 75 | Type: "Windows10", 76 | }, 77 | User: mcTokenUser{ 78 | Language: "en", 79 | LanguageCode: "en-US", 80 | RegionCode: "US", 81 | Token: xblToken, 82 | TokenType: "Xbox", 83 | }, 84 | }, nil) 85 | if err != nil { 86 | return nil, err 87 | } 88 | return &resp.Result, nil 89 | } 90 | 91 | func doRequest[T any](ctx context.Context, client *http.Client, method, url string, payload any, extraHeaders func(*http.Request)) (*T, error) { 92 | body, err := json.Marshal(payload) 93 | if err != nil { 94 | return nil, err 95 | } 96 | req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) 97 | if err != nil { 98 | return nil, err 99 | } 100 | req.Header.Set("Accept", "*/*") 101 | req.Header.Set("Content-Type", "application/json") 102 | req.Header.Set("User-Agent", minecraftUserAgent) 103 | req.Header.Set("Accept-Language", "en-US,en;q=0.5") 104 | req.Header.Set("Cache-Control", "no-cache") 105 | if extraHeaders != nil { 106 | extraHeaders(req) 107 | } 108 | 109 | res, err := client.Do(req) 110 | if err != nil { 111 | return nil, err 112 | } 113 | defer res.Body.Close() 114 | 115 | if res.StatusCode >= 400 { 116 | bodyResp, err := io.ReadAll(res.Body) 117 | if err != nil { 118 | return nil, err 119 | } 120 | var resp map[string]any 121 | err = json.Unmarshal(bodyResp, &resp) 122 | if err != nil { 123 | return nil, err 124 | } 125 | return nil, &JsonResponseError{ 126 | Status: res.Status, 127 | Data: resp, 128 | } 129 | } 130 | 131 | var resp T 132 | err = json.NewDecoder(res.Body).Decode(&resp) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return &resp, nil 137 | } 138 | -------------------------------------------------------------------------------- /utils/discovery/gatherings.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/sandertv/gophertunnel/minecraft/protocol" 11 | ) 12 | 13 | type Segment struct { 14 | SegmentType string `json:"segmentType"` 15 | StartTimeUtc time.Time `json:"startTimeUtc"` 16 | EndTimeUtc time.Time `json:"endTimeUtc"` 17 | UI struct { 18 | CaptionText string `json:"captionText"` 19 | CaptionForegroundColor string `json:"captionForegroundColor"` 20 | CaptionBackgroundColor string `json:"captionBackgroundColor"` 21 | StartScreenButtonText string `json:"startScreenButtonText"` 22 | BadgeImage string `json:"badgeImage"` 23 | CaptionIncludesCountdown bool `json:"captionIncludesCountdown"` 24 | ActionButtonText string `json:"actionButtonText"` 25 | InfoButtonText string `json:"infoButtonText"` 26 | HeaderText string `json:"headerText"` 27 | TitleText string `json:"titleText"` 28 | BodyText string `json:"bodyText"` 29 | EventImage string `json:"eventImage"` 30 | BodyImage string `json:"bodyImage"` 31 | ActionButtonURL string `json:"actionButtonUrl"` 32 | InfoButtonURL string `json:"infoButtonUrl"` 33 | } `json:"ui"` 34 | } 35 | 36 | type Gathering struct { 37 | client *GatheringsService 38 | 39 | GatheringID string `json:"gatheringId"` 40 | StartTimeUtc time.Time `json:"startTimeUtc"` 41 | EndTimeUtc time.Time `json:"endTimeUtc"` 42 | Segments []Segment `json:"segments"` 43 | Title string `json:"title"` 44 | Description string `json:"description"` 45 | IsEnabled bool `json:"isEnabled"` 46 | IsPrivate bool `json:"isPrivate"` 47 | GatheringType string `json:"gatheringType"` 48 | AdditionalLoc map[string]any `json:"additionalLoc"` 49 | } 50 | 51 | func (g *Gathering) Address(ctx context.Context) (string, error) { 52 | type venueResponse struct { 53 | Result struct { 54 | Venue struct { 55 | ServerIpAddress string `json:"serverIpAddress"` 56 | ServerPort int `json:"serverPort"` 57 | } `json:"venue"` 58 | } `json:"result"` 59 | } 60 | 61 | resp1, err := doRequest[map[string]any](ctx, http.DefaultClient, "GET", 62 | fmt.Sprintf("%s/api/v1.0/access?lang=en-US&clientVersion=%s&clientPlatform=Windows10&clientSubPlatform=Windows10", g.client.ServiceURI, protocol.CurrentVersion), 63 | nil, g.client.mcTokenAuth, 64 | ) 65 | if err != nil { 66 | return "", err 67 | } 68 | _ = resp1 69 | 70 | resp, err := doRequest[venueResponse](ctx, http.DefaultClient, "GET", 71 | fmt.Sprintf("%s/api/v1.0/venue/%s", g.client.ServiceURI, g.GatheringID), 72 | nil, g.client.mcTokenAuth, 73 | ) 74 | if err != nil { 75 | return "", err 76 | } 77 | 78 | if resp.Result.Venue.ServerIpAddress == "" { 79 | return "", errors.New("didnt get a server address") 80 | } 81 | 82 | return fmt.Sprintf("%s:%d", resp.Result.Venue.ServerIpAddress, resp.Result.Venue.ServerPort), nil 83 | } 84 | 85 | type GatheringsService struct { 86 | Service 87 | token *MCToken 88 | } 89 | 90 | func (g *GatheringsService) SetToken(token *MCToken) { 91 | g.token = token 92 | } 93 | 94 | func (g *GatheringsService) Gatherings(ctx context.Context) ([]*Gathering, error) { 95 | type gatheringsResponse struct { 96 | Result []Gathering `json:"result"` 97 | } 98 | 99 | resp, err := doRequest[gatheringsResponse](ctx, http.DefaultClient, "GET", 100 | fmt.Sprintf("%s/api/v1.0/config/public?lang=en-GB&clientVersion=%s&clientPlatform=Windows10&clientSubPlatform=Windows10", g.ServiceURI, protocol.CurrentVersion), 101 | nil, g.mcTokenAuth, 102 | ) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | var gatherings []*Gathering 108 | for _, gathering := range resp.Result { 109 | gathering.client = g 110 | gatherings = append(gatherings, &gathering) 111 | } 112 | return gatherings, nil 113 | } 114 | 115 | func (g *GatheringsService) mcTokenAuth(req *http.Request) { 116 | req.Header.Set("Authorization", g.token.AuthorizationHeader) 117 | } 118 | 119 | type JsonResponseError struct { 120 | Status string 121 | Data map[string]any 122 | } 123 | 124 | func (e JsonResponseError) Error() string { 125 | message, ok := e.Data["message"].(string) 126 | if ok { 127 | return e.Status + "\n" + message 128 | } 129 | return e.Status 130 | } 131 | -------------------------------------------------------------------------------- /utils/folders.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/bedrock-tool/bedrocktool/utils/osabs" 7 | ) 8 | 9 | func PathCache(p ...string) string { 10 | pj := path.Join(p...) 11 | if path.IsAbs(pj) { 12 | return pj 13 | } 14 | return path.Join(osabs.GetCacheDir(), pj) 15 | } 16 | 17 | func PathData(p ...string) string { 18 | pj := path.Join(p...) 19 | if path.IsAbs(pj) { 20 | return pj 21 | } 22 | return path.Join(osabs.GetDataDir(), pj) 23 | } 24 | -------------------------------------------------------------------------------- /utils/fswriter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "archive/zip" 5 | "compress/flate" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "path" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | type WriterFS interface { 15 | Create(filename string) (w io.WriteCloser, err error) 16 | } 17 | 18 | func SubFS(base WriterFS, dir string) WriterFS { 19 | return &subFS{base: base, dir: dir} 20 | } 21 | 22 | type subFS struct { 23 | base WriterFS 24 | dir string 25 | } 26 | 27 | func (s *subFS) Create(filename string) (w io.WriteCloser, err error) { 28 | filename = path.Clean(filename) 29 | filename = path.Join(s.dir, filename) 30 | return s.base.Create(filename) 31 | } 32 | 33 | func CopyFS(src fs.FS, dst WriterFS) error { 34 | return fs.WalkDir(src, ".", func(fpath string, d fs.DirEntry, err error) error { 35 | if err != nil { 36 | return err 37 | } 38 | if d.IsDir() { 39 | return nil 40 | } 41 | r, err := src.Open(fpath) 42 | if err != nil { 43 | return err 44 | } 45 | defer r.Close() 46 | w, err := dst.Create(fpath) 47 | if err != nil { 48 | return err 49 | } 50 | defer w.Close() 51 | _, err = io.Copy(w, r) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | }) 57 | } 58 | 59 | type OSWriter struct { 60 | Base string 61 | } 62 | 63 | func (o OSWriter) Create(filename string) (w io.WriteCloser, err error) { 64 | base := strings.ReplaceAll(o.Base, "\\", "/") 65 | filename = strings.ReplaceAll(filename, "../", "") 66 | fullpath := path.Join(base, path.Clean(filename)) 67 | err = os.MkdirAll(path.Dir(fullpath), 0777) 68 | if err != nil { 69 | return nil, err 70 | } 71 | w, err = os.Create(fullpath) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return w, err 76 | } 77 | 78 | var deflatePool = sync.Pool{ 79 | New: func() any { 80 | w, _ := flate.NewWriter(nil, flate.HuffmanOnly) 81 | return w 82 | }, 83 | } 84 | 85 | type closePutback struct { 86 | *flate.Writer 87 | } 88 | 89 | func (c *closePutback) Close() error { 90 | if c.Writer == nil { 91 | return nil 92 | } 93 | err := c.Writer.Close() 94 | if err != nil { 95 | return err 96 | } 97 | deflatePool.Put(c.Writer) 98 | c.Writer = nil 99 | return nil 100 | } 101 | 102 | func ZipCompressPool(zw *zip.Writer) { 103 | zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) { 104 | w := deflatePool.Get().(*flate.Writer) 105 | w.Reset(out) 106 | return &closePutback{w}, nil 107 | }) 108 | } 109 | 110 | type ZipWriter struct { 111 | Writer *zip.Writer 112 | } 113 | 114 | func (z ZipWriter) Create(filename string) (w io.WriteCloser, err error) { 115 | zw, err := z.Writer.Create(filename) 116 | return nullCloser{zw}, err 117 | } 118 | 119 | type nullCloser struct { 120 | io.Writer 121 | } 122 | 123 | func (nullCloser) Close() error { 124 | return nil 125 | } 126 | 127 | type MultiWriterFS struct { 128 | FSs []WriterFS 129 | } 130 | 131 | func (m MultiWriterFS) Create(filename string) (w io.WriteCloser, err error) { 132 | var files []io.Writer 133 | var closers []func() error 134 | for _, fs := range m.FSs { 135 | f, err := fs.Create(filename) 136 | if err != nil { 137 | for _, f := range files { 138 | f := f.(io.Closer) 139 | f.Close() 140 | } 141 | return nil, err 142 | } 143 | files = append(files, f) 144 | closers = append(closers, f.Close) 145 | } 146 | 147 | return multiCloser{ 148 | Writer: io.MultiWriter(files...), 149 | closers: closers, 150 | }, nil 151 | } 152 | 153 | type multiCloser struct { 154 | io.Writer 155 | closers []func() error 156 | } 157 | 158 | func (m multiCloser) Close() error { 159 | for _, close := range m.closers { 160 | close() 161 | } 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /utils/images.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "unsafe" 7 | ) 8 | 9 | func Img2rgba(img *image.RGBA) []color.RGBA { 10 | return unsafe.Slice((*color.RGBA)(unsafe.Pointer(unsafe.SliceData(img.Pix))), len(img.Pix)/4) 11 | } 12 | func RGBA2Img(colors []color.RGBA, width, height int) *image.RGBA { 13 | return &image.RGBA{ 14 | Pix: unsafe.Slice((*uint8)(unsafe.Pointer(unsafe.SliceData(colors))), len(colors)*4), 15 | Rect: image.Rect(0, 0, width, height), 16 | Stride: 4 * width, 17 | } 18 | } 19 | 20 | // LERP is a linear interpolation function 21 | func LERP(p1, p2, alpha float64) float64 { 22 | return (1-alpha)*p1 + alpha*p2 23 | } 24 | 25 | func blendColorValue(c1, c2, a uint8) uint8 { 26 | return uint8(LERP(float64(c1), float64(c2), float64(a)/float64(0xff))) 27 | } 28 | 29 | func blendAlphaValue(a1, a2 uint8) uint8 { 30 | return uint8(LERP(float64(a1), float64(0xff), float64(a2)/float64(0xff))) 31 | } 32 | 33 | func BlendColors(c1, c2 color.RGBA) (ret color.RGBA) { 34 | ret.R = blendColorValue(c1.R, c2.R, c2.A) 35 | ret.G = blendColorValue(c1.G, c2.G, c2.A) 36 | ret.B = blendColorValue(c1.B, c2.B, c2.A) 37 | ret.A = blendAlphaValue(c1.A, c2.A) 38 | return ret 39 | } 40 | 41 | // DrawImgScaledPos draws src onto dst at bottomLeft, scaled to size 42 | func DrawImgScaledPos(dst *image.RGBA, src *image.RGBA, bottomLeft image.Point, sizeScaled int) { 43 | if src == nil || dst == nil { 44 | panic("nil src or dst") 45 | } 46 | sbx := src.Bounds().Dx() 47 | ratio := int(float64(sbx) / float64(sizeScaled)) 48 | 49 | for xOut := bottomLeft.X; xOut < bottomLeft.X+sizeScaled; xOut++ { 50 | for yOut := bottomLeft.Y; yOut < bottomLeft.Y+sizeScaled; yOut++ { 51 | xIn := (xOut - bottomLeft.X) * ratio 52 | yIn := (yOut - bottomLeft.Y) * ratio 53 | dst.Set(xOut, yOut, src.At(xIn, yIn)) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /utils/input.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/sirupsen/logrus" 11 | 12 | "github.com/chzyer/readline" 13 | ) 14 | 15 | func UserInput(ctx context.Context, q string, validator func(string) bool) (string, bool) { 16 | inst, err := readline.New(q) 17 | if err != nil { 18 | panic(err) 19 | } 20 | line, err := inst.Readline() 21 | switch { 22 | case err == io.EOF: 23 | return "", true 24 | case err == readline.ErrInterrupt: 25 | return "", true 26 | case err != nil: 27 | logrus.Error(err) 28 | return "", true 29 | default: 30 | return line, false 31 | } 32 | } 33 | 34 | var ( 35 | realmRegex = regexp.MustCompile("realm:(?P.*)") 36 | pcapRegex = regexp.MustCompile(`(?P(?P.*)\.pcap2)(?:\?(?P.*))?`) 37 | gatheringRegex = regexp.MustCompile("gathering:(?P.*)+") 38 | ) 39 | 40 | func regexGetParams(r *regexp.Regexp, s string) (params map[string]string) { 41 | match := r.FindStringSubmatch(s) 42 | params = make(map[string]string) 43 | for i, name := range r.SubexpNames() { 44 | if i > 0 && i <= len(match) { 45 | params[name] = match[i] 46 | } 47 | } 48 | return params 49 | } 50 | 51 | func ValidateServerInput(server string) bool { 52 | if pcapRegex.MatchString(server) { 53 | return true 54 | } 55 | 56 | if realmRegex.MatchString(server) { 57 | return true // todo 58 | } 59 | 60 | if gatheringRegex.MatchString(server) { 61 | return true 62 | } 63 | 64 | host, _, err := net.SplitHostPort(server) 65 | if err != nil { 66 | if strings.Contains(err.Error(), "missing port in address") { 67 | host = server 68 | } 69 | } 70 | 71 | ip := net.ParseIP(host) 72 | if ip != nil { 73 | return true 74 | } 75 | 76 | ips, _ := net.LookupIP(host) 77 | return len(ips) > 0 78 | } 79 | -------------------------------------------------------------------------------- /utils/invalid_json_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/bedrock-tool/bedrocktool/utils" 8 | ) 9 | 10 | var invalidJson = []byte(`{"test": 001 /* comment */}frgvejmdorgvm`) 11 | 12 | func TestInvalidJsonFix(t *testing.T) { 13 | var test struct { 14 | Test int `json:"test"` 15 | } 16 | err := utils.ParseJson(invalidJson, &test) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | fmt.Printf("%+#v\n", test) 22 | } 23 | -------------------------------------------------------------------------------- /utils/merge/block_registry.go: -------------------------------------------------------------------------------- 1 | package merge 2 | 3 | import ( 4 | _ "unsafe" 5 | 6 | _ "github.com/df-mc/dragonfly/server/block" 7 | "github.com/df-mc/dragonfly/server/world" 8 | ) 9 | 10 | type BlockRegistry struct { 11 | world.BlockRegistry 12 | Rids map[uint32]Block 13 | } 14 | 15 | type Block struct { 16 | name string 17 | properties map[string]any 18 | } 19 | 20 | //go:linkname networkBlockHash github.com/df-mc/dragonfly/server/world.networkBlockHash 21 | func networkBlockHash(name string, properties map[string]any) uint32 22 | 23 | func (b *BlockRegistry) RuntimeIDToState(runtimeID uint32) (name string, properties map[string]any, found bool) { 24 | name, properties, found = b.BlockRegistry.RuntimeIDToState(runtimeID) 25 | if found { 26 | return 27 | } 28 | block := b.Rids[runtimeID] 29 | return block.name, block.properties, true 30 | } 31 | 32 | func (b *BlockRegistry) StateToRuntimeID(name string, properties map[string]any) (runtimeID uint32, found bool) { 33 | runtimeID, found = b.BlockRegistry.StateToRuntimeID(name, properties) 34 | if found { 35 | return 36 | } 37 | runtimeID = networkBlockHash(name, properties) 38 | b.Rids[runtimeID] = Block{name, properties} 39 | return runtimeID, true 40 | } 41 | 42 | func (b *BlockRegistry) BlockByRuntimeID(rid uint32) (world.Block, bool) { 43 | block, ok := b.BlockRegistry.BlockByRuntimeID(rid) 44 | if ok { 45 | return block, true 46 | } 47 | block2, ok := b.Rids[rid] 48 | return world.UnknownBlock{ 49 | BlockState: world.BlockState{ 50 | Name: block2.name, 51 | Properties: block2.properties, 52 | }, 53 | }, ok 54 | } 55 | func (b *BlockRegistry) BlockRuntimeID(block world.Block) (rid uint32) { 56 | name, properties := block.EncodeBlock() 57 | return networkBlockHash(name, properties) 58 | } 59 | 60 | func (b *BlockRegistry) BlockCount() int { 61 | return b.BlockRegistry.BlockCount() + len(b.Rids) 62 | } 63 | 64 | func (b *BlockRegistry) RandomTickBlock(rid uint32) bool { 65 | if _, ok := b.Rids[rid]; ok { 66 | return false 67 | } 68 | return b.BlockRegistry.RandomTickBlock(rid) 69 | } 70 | 71 | func (b *BlockRegistry) FilteringBlock(rid uint32) uint8 { 72 | if _, ok := b.Rids[rid]; ok { 73 | return 15 74 | } 75 | return b.BlockRegistry.FilteringBlock(rid) 76 | } 77 | 78 | func (b *BlockRegistry) LightBlock(rid uint32) uint8 { 79 | if _, ok := b.Rids[rid]; ok { 80 | return 0 81 | } 82 | return b.BlockRegistry.LightBlock(rid) 83 | } 84 | 85 | func (b *BlockRegistry) NBTBlock(rid uint32) bool { 86 | if _, ok := b.Rids[rid]; ok { 87 | return false 88 | } 89 | return b.BlockRegistry.NBTBlock(rid) 90 | } 91 | 92 | func (b *BlockRegistry) LiquidDisplacingBlock(rid uint32) bool { 93 | if _, ok := b.Rids[rid]; ok { 94 | return true 95 | } 96 | return b.BlockRegistry.LiquidDisplacingBlock(rid) 97 | } 98 | 99 | func (b *BlockRegistry) LiquidBlock(rid uint32) bool { 100 | if _, ok := b.Rids[rid]; ok { 101 | return false 102 | } 103 | return b.BlockRegistry.LiquidBlock(rid) 104 | } 105 | -------------------------------------------------------------------------------- /utils/nbtconv/colour.go: -------------------------------------------------------------------------------- 1 | package nbtconv 2 | 3 | import ( 4 | "encoding/binary" 5 | "image/color" 6 | ) 7 | 8 | // Int32FromRGBA converts a color.RGBA into an int32. These int32s are present in, for example, signs. 9 | func Int32FromRGBA(x color.RGBA) int32 { 10 | if x.R == 0 && x.G == 0 && x.B == 0 { 11 | // Default to black colour. The default (0x000000) is a transparent colour. Text with this colour will not show 12 | // up on the sign. 13 | return int32(-0x1000000) 14 | } 15 | return int32(binary.BigEndian.Uint32([]byte{x.A, x.R, x.G, x.B})) 16 | } 17 | 18 | // RGBAFromInt32 converts an int32 into a color.RGBA. These int32s are present in, for example, signs. 19 | func RGBAFromInt32(x int32) color.RGBA { 20 | b := make([]byte, 4) 21 | binary.BigEndian.PutUint32(b, uint32(x)) 22 | 23 | return color.RGBA{A: b[0], R: b[1], G: b[2], B: b[3]} 24 | } 25 | -------------------------------------------------------------------------------- /utils/nbtconv/item.go: -------------------------------------------------------------------------------- 1 | package nbtconv 2 | 3 | import ( 4 | "github.com/df-mc/dragonfly/server/item/inventory" 5 | ) 6 | 7 | // InvFromNBT decodes the data of an NBT slice into the inventory passed. 8 | func InvFromNBT(inv *inventory.Inventory, items []any) { 9 | for _, itemData := range items { 10 | data, _ := itemData.(map[string]any) 11 | it := Item(data, nil) 12 | if it.Empty() { 13 | continue 14 | } 15 | _ = inv.SetItem(int(Uint8(data, "Slot")), it) 16 | } 17 | } 18 | 19 | // InvToNBT encodes an inventory to a data slice which may be encoded as NBT. 20 | func InvToNBT(inv *inventory.Inventory) []map[string]any { 21 | var items []map[string]any 22 | for index, i := range inv.Slots() { 23 | if i.Empty() { 24 | continue 25 | } 26 | data := WriteItem(i, true) 27 | data["Slot"] = byte(index) 28 | items = append(items, data) 29 | } 30 | return items 31 | } 32 | -------------------------------------------------------------------------------- /utils/nbtconv/write.go: -------------------------------------------------------------------------------- 1 | package nbtconv 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "sort" 7 | 8 | "github.com/df-mc/dragonfly/server/item" 9 | "github.com/df-mc/dragonfly/server/world" 10 | "github.com/df-mc/dragonfly/server/world/chunk" 11 | ) 12 | 13 | // WriteItem encodes an item stack into a map that can be encoded using NBT. 14 | func WriteItem(s item.Stack, disk bool) map[string]any { 15 | tag := make(map[string]any) 16 | if nbt, ok := s.Item().(world.NBTer); ok { 17 | for k, v := range nbt.EncodeNBT() { 18 | tag[k] = v 19 | } 20 | } 21 | writeAnvilCost(tag, s) 22 | writeDamage(tag, s, disk) 23 | writeDisplay(tag, s) 24 | writeDragonflyData(tag, s) 25 | writeEnchantments(tag, s) 26 | 27 | data := make(map[string]any) 28 | if disk { 29 | writeItemStack(data, tag, s) 30 | } else { 31 | for k, v := range tag { 32 | data[k] = v 33 | } 34 | } 35 | return data 36 | } 37 | 38 | // WriteBlock encodes a world.Block into a map that can be encoded using NBT. 39 | func WriteBlock(b world.Block) map[string]any { 40 | name, properties := b.EncodeBlock() 41 | return map[string]any{ 42 | "name": name, 43 | "states": properties, 44 | "version": chunk.CurrentBlockVersion, 45 | } 46 | } 47 | 48 | // writeItemStack writes the name, metadata value, count and NBT of an item to a map ready for NBT encoding. 49 | func writeItemStack(m, t map[string]any, s item.Stack) { 50 | m["Name"], m["Damage"] = s.Item().EncodeItem() 51 | if b, ok := s.Item().(world.Block); ok { 52 | v := map[string]any{} 53 | writeBlock(v, b) 54 | m["Block"] = v 55 | } 56 | m["Count"] = byte(s.Count()) 57 | if len(t) > 0 { 58 | m["tag"] = t 59 | } 60 | } 61 | 62 | // writeBlock writes the name, properties and version of a block to a map ready for NBT encoding. 63 | func writeBlock(m map[string]any, b world.Block) { 64 | m["name"], m["states"] = b.EncodeBlock() 65 | m["version"] = chunk.CurrentBlockVersion 66 | } 67 | 68 | // writeDragonflyData writes additional data associated with an item.Stack to a map for NBT encoding. 69 | func writeDragonflyData(m map[string]any, s item.Stack) { 70 | if v := s.Values(); len(v) != 0 { 71 | buf := new(bytes.Buffer) 72 | if err := gob.NewEncoder(buf).Encode(mapToSlice(v)); err != nil { 73 | panic("error encoding item user data: " + err.Error()) 74 | } 75 | m["dragonflyData"] = buf.Bytes() 76 | } 77 | } 78 | 79 | // mapToSlice converts a map to a slice of the type mapValue and orders the slice by the keys in the map to ensure a 80 | // deterministic order. 81 | func mapToSlice(m map[string]any) []mapValue { 82 | values := make([]mapValue, 0, len(m)) 83 | for k, v := range m { 84 | values = append(values, mapValue{K: k, V: v}) 85 | } 86 | sort.Slice(values, func(i, j int) bool { 87 | return values[i].K < values[j].K 88 | }) 89 | return values 90 | } 91 | 92 | // mapValue represents a value in a map. It is used to convert maps to a slice and order the slice before encoding to 93 | // NBT to ensure a deterministic output. 94 | type mapValue struct { 95 | K string 96 | V any 97 | } 98 | 99 | // writeEnchantments writes the enchantments of an item to a map for NBT encoding. 100 | func writeEnchantments(m map[string]any, s item.Stack) { 101 | if len(s.Enchantments()) != 0 { 102 | var enchantments []map[string]any 103 | for _, e := range s.Enchantments() { 104 | if eType, ok := item.EnchantmentID(e.Type()); ok { 105 | enchantments = append(enchantments, map[string]any{ 106 | "id": int16(eType), 107 | "lvl": int16(e.Level()), 108 | }) 109 | } 110 | } 111 | m["ench"] = enchantments 112 | } 113 | } 114 | 115 | func writeTrim(m map[string]any, t item.ArmourTrim) { 116 | if !t.Zero() { 117 | m["Trim"] = map[string]any{ 118 | "Material": t.Material.TrimMaterial(), 119 | "Pattern": t.Template.String(), 120 | } 121 | } 122 | } 123 | 124 | // writeDisplay writes the display name and lore of an item to a map for NBT encoding. 125 | func writeDisplay(m map[string]any, s item.Stack) { 126 | name, lore := s.CustomName(), s.Lore() 127 | v := map[string]any{} 128 | if name != "" { 129 | v["Name"] = name 130 | } 131 | if len(lore) != 0 { 132 | v["Lore"] = lore 133 | } 134 | if len(v) != 0 { 135 | m["display"] = v 136 | } 137 | } 138 | 139 | // writeDamage writes the damage to an item.Stack (either an int16 for disk or int32 for network) to a map for NBT 140 | // encoding. 141 | func writeDamage(m map[string]any, s item.Stack, disk bool) { 142 | if v, ok := m["Damage"]; !ok || v.(int16) == 0 { 143 | if _, ok := s.Item().(item.Durable); ok { 144 | if disk { 145 | m["Damage"] = int16(s.MaxDurability() - s.Durability()) 146 | } else { 147 | m["Damage"] = int32(s.MaxDurability() - s.Durability()) 148 | } 149 | } 150 | } 151 | } 152 | 153 | // writeAnvilCost ... 154 | func writeAnvilCost(m map[string]any, s item.Stack) { 155 | if cost := s.AnvilCost(); cost > 0 { 156 | m["RepairCost"] = int32(cost) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /utils/net.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | _ "gioui.org/app/permission/networkstate" 9 | ) 10 | 11 | func getLocalIp2() (string, error) { 12 | nc, err := net.Dial("tcp4", "1.1.1.1:80") 13 | if err != nil { 14 | return "", err 15 | } 16 | defer nc.Close() 17 | return nc.LocalAddr().(*net.TCPAddr).IP.String(), nil 18 | } 19 | 20 | func GetLocalIP() (string, error) { 21 | interfaces, err := net.Interfaces() 22 | if err != nil { 23 | return getLocalIp2() 24 | } 25 | 26 | for _, iface := range interfaces { 27 | if iface.Flags&net.FlagLoopback != 0 { 28 | continue 29 | } 30 | if iface.Flags&net.FlagUp == 0 { 31 | continue 32 | } 33 | if iface.Flags&net.FlagMulticast == 0 { 34 | continue 35 | } 36 | nameLower := strings.ToLower(iface.Name) 37 | if strings.Contains(nameLower, "tun") || 38 | strings.Contains(nameLower, "tap") || 39 | strings.Contains(nameLower, "vpn") || 40 | strings.Contains(nameLower, "wg") || 41 | strings.Contains(nameLower, "tailscale") || 42 | strings.Contains(nameLower, "vethernet") { 43 | continue 44 | } 45 | 46 | addrs, err := iface.Addrs() 47 | if err != nil { 48 | continue 49 | } 50 | 51 | for _, addr := range addrs { 52 | ipNet, ok := addr.(*net.IPNet) 53 | if !ok { 54 | continue 55 | } 56 | if ipNet.IP.To4() != nil && !ipNet.IP.IsLoopback() { 57 | return ipNet.IP.String(), nil 58 | } 59 | } 60 | } 61 | 62 | return "", fmt.Errorf("no suitable local IP address found") 63 | } 64 | -------------------------------------------------------------------------------- /utils/netisolation_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package utils 4 | 5 | func Netisolation() error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /utils/netisolation_windows.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | 7 | "github.com/sirupsen/logrus" 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | const ( 12 | NETISO_FLAG_FORCE_COMPUTE_BINARIES = 0x1 13 | ) 14 | 15 | type SID_AND_ATTRIBUTES struct { 16 | appContainerSid *windows.SID 17 | attributes uint32 18 | } 19 | 20 | type INET_FIREWALL_AC_CAPABILITIES struct { 21 | count uint32 22 | capabilities *SID_AND_ATTRIBUTES 23 | } 24 | 25 | type INET_FIREWALL_AC_BINARIES struct { 26 | count uint32 27 | binaries *uint16 28 | } 29 | 30 | type AppContainer struct { 31 | appContainerSid *windows.SID 32 | userSid *windows.SID 33 | appContainerName *uint16 34 | displayName *uint16 35 | description *uint16 36 | capabilities INET_FIREWALL_AC_CAPABILITIES 37 | binaries INET_FIREWALL_AC_BINARIES 38 | workingDirectory *uint16 39 | packageFullName *uint16 40 | } 41 | 42 | var ( 43 | modFirewallapi = windows.NewLazySystemDLL("Firewallapi.dll") 44 | NetworkIsolationEnumAppContainers = modFirewallapi.NewProc("NetworkIsolationEnumAppContainers") 45 | NetworkIsolationGetAppContainerConfig = modFirewallapi.NewProc("NetworkIsolationGetAppContainerConfig") 46 | NetworkIsolationSetAppContainerConfig = modFirewallapi.NewProc("NetworkIsolationSetAppContainerConfig") 47 | NetworkIsolationFreeAppContainers = modFirewallapi.NewProc("NetworkIsolationFreeAppContainers") 48 | ) 49 | 50 | func Netisolation() error { 51 | var count uint32 52 | var array *AppContainer 53 | ret, _, err := NetworkIsolationEnumAppContainers.Call( 54 | uintptr(NETISO_FLAG_FORCE_COMPUTE_BINARIES), 55 | uintptr(unsafe.Pointer(&count)), 56 | uintptr(unsafe.Pointer(&array)), 57 | ) 58 | if ret != 0 { 59 | return fmt.Errorf("failed to enumerate app containers: %w", err) 60 | } 61 | defer NetworkIsolationFreeAppContainers.Call(uintptr(unsafe.Pointer(array))) 62 | 63 | var countConf uint32 64 | var arrayConf *SID_AND_ATTRIBUTES 65 | ret, _, err = NetworkIsolationGetAppContainerConfig.Call(uintptr(unsafe.Pointer(&countConf)), uintptr(unsafe.Pointer(&arrayConf))) 66 | if ret != 0 { 67 | return fmt.Errorf("failed to get app container configs: %w", err) 68 | } 69 | config := unsafe.Slice(arrayConf, countConf) 70 | 71 | for _, ac := range unsafe.Slice(array, count) { 72 | moniker := windows.UTF16PtrToString(ac.appContainerName) 73 | displayName := windows.UTF16PtrToString(ac.displayName) 74 | 75 | if moniker == "Microsoft.MinecraftUWP_8wekyb3d8bbwe" { 76 | for _, conf := range config { 77 | if conf.appContainerSid.Equals(ac.appContainerSid) { 78 | //logrus.Info("NetIsolation Loopback was already configured") 79 | return nil 80 | } 81 | } 82 | config = append(config, SID_AND_ATTRIBUTES{ 83 | appContainerSid: ac.appContainerSid, 84 | attributes: 0, 85 | }) 86 | 87 | _, _, err := NetworkIsolationSetAppContainerConfig.Call(uintptr(len(config)), uintptr(unsafe.Pointer(unsafe.SliceData(config)))) 88 | if err != windows.NOERROR { 89 | return fmt.Errorf("failed to set app container configs: %w", err) 90 | } 91 | logrus.Infof("NetIsolation Loopback allowed for \"%s\"", displayName) 92 | return nil 93 | } 94 | } 95 | 96 | //logrus.Info("You dont have Minecraft Bedrock installed") 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /utils/osabs/osabs.go: -------------------------------------------------------------------------------- 1 | package osabs 2 | 3 | var cacheDir string 4 | var dataDir string 5 | 6 | func GetCacheDir() string { 7 | return cacheDir 8 | } 9 | 10 | func GetDataDir() string { 11 | return dataDir 12 | } 13 | 14 | func Init() error { 15 | return implInit() 16 | } 17 | -------------------------------------------------------------------------------- /utils/osabs/osabs_android.go: -------------------------------------------------------------------------------- 1 | package osabs 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gioui.org/app" 7 | "git.wow.st/gmp/jni" 8 | ) 9 | 10 | func implInit() error { 11 | return jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error { 12 | // classes 13 | ctxClass := jni.FindClass(env, "android/content/Context") 14 | if ctxClass == 0 { 15 | return fmt.Errorf("failed to find Context class") 16 | } 17 | fileClass := jni.FindClass(env, "java/io/File") 18 | if fileClass == 0 { 19 | return fmt.Errorf("failed to find File class") 20 | } 21 | 22 | appCtx := jni.Object(app.AppContext()) 23 | if appCtx == 0 { 24 | return fmt.Errorf("failed to get AppContext") 25 | } 26 | 27 | // methods 28 | getCacheDirMethod := jni.GetMethodID(env, ctxClass, "getCacheDir", "()Ljava/io/File;") 29 | if getCacheDirMethod == nil { 30 | return fmt.Errorf("failed to find getCacheDir") 31 | } 32 | 33 | getExternalFilesDirMethod := jni.GetMethodID(env, ctxClass, "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;") 34 | if getExternalFilesDirMethod == nil { 35 | return fmt.Errorf("failed to find getExternalFilesDir") 36 | } 37 | 38 | getAbsolutePathMethod := jni.GetMethodID(env, fileClass, "getAbsolutePath", "()Ljava/lang/String;") 39 | if getAbsolutePathMethod == nil { 40 | return fmt.Errorf("failed to find getAbsolutePath") 41 | } 42 | 43 | fmt.Printf("calling getCacheDir\n") 44 | cacheDirFileObj, err := jni.CallObjectMethod(env, appCtx, getCacheDirMethod) 45 | if err != nil { 46 | return fmt.Errorf("failed to call getCacheDir: %w", err) 47 | } 48 | 49 | { // cache dir 50 | fmt.Printf("calling getAbsolutePath\n") 51 | pathStrObj, err := jni.CallObjectMethod(env, cacheDirFileObj, getAbsolutePathMethod) 52 | if err != nil { 53 | return fmt.Errorf("failed to call getAbsolutePath: %w", err) 54 | } 55 | cacheDir = jni.GoString(env, jni.String(pathStrObj)) 56 | } 57 | 58 | { // data dir 59 | fmt.Printf("calling getExternalFilesDir\n") 60 | filesDirFileObj, err := jni.CallObjectMethod(env, appCtx, getExternalFilesDirMethod, 0) 61 | if err != nil { 62 | return fmt.Errorf("failed to call getExternalFilesDir: %w", err) 63 | } 64 | 65 | fmt.Printf("calling getAbsolutePath\n") 66 | pathStrObj, err := jni.CallObjectMethod(env, filesDirFileObj, getAbsolutePathMethod) 67 | if err != nil { 68 | return fmt.Errorf("failed to call getAbsolutePath: %w", err) 69 | } 70 | dataDir = jni.GoString(env, jni.String(pathStrObj)) 71 | } 72 | 73 | return nil 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /utils/osabs/osabs_linux.go: -------------------------------------------------------------------------------- 1 | //go:build !android 2 | 3 | package osabs 4 | 5 | func implInit() error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /utils/osabs/osabs_windows.go: -------------------------------------------------------------------------------- 1 | package osabs 2 | 3 | func implInit() error { 4 | return nil 5 | } 6 | -------------------------------------------------------------------------------- /utils/panic.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | 8 | "github.com/bedrock-tool/bedrocktool/locale" 9 | ) 10 | 11 | var panicStack string 12 | var panicErr error 13 | 14 | func PrintPanic(err error) { 15 | panicStack = string(debug.Stack()) 16 | panicErr = err 17 | fmt.Println(locale.Loc("fatal_error", nil)) 18 | fmt.Println("--COPY FROM HERE--") 19 | fmt.Printf("Version: %s\n", Version) 20 | fmt.Printf("Cmdline: %s\n", os.Args) 21 | fmt.Printf("Error: %s\n", err) 22 | fmt.Println("stacktrace from panic: \n" + panicStack) 23 | fmt.Println("--END COPY HERE--") 24 | fmt.Println("") 25 | fmt.Println(locale.Loc("report_issue", nil)) 26 | fmt.Println(locale.Loc("used_extra_debug_report", nil)) 27 | } 28 | -------------------------------------------------------------------------------- /utils/playfab/playfab.go: -------------------------------------------------------------------------------- 1 | //go:build false 2 | 3 | package playfab 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "github.com/bedrock-tool/bedrocktool/utils/discovery" 15 | "github.com/bedrock-tool/bedrocktool/utils/xbox" 16 | "golang.org/x/oauth2" 17 | ) 18 | 19 | const minecraftUserAgent = "libhttpclient/1.0.0.0" 20 | const minecraftDefaultSDK = "XPlatCppSdk-3.6.190304" 21 | 22 | type Client struct { 23 | src oauth2.TokenSource 24 | http *http.Client 25 | discovery *discovery.Discovery 26 | 27 | accountID string 28 | sessionTicket string 29 | 30 | playerToken *EntityToken 31 | masterToken *EntityToken 32 | } 33 | 34 | func NewClient(discovery *discovery.Discovery, src oauth2.TokenSource) *Client { 35 | return &Client{ 36 | src: src, 37 | http: http.DefaultClient, 38 | discovery: discovery, 39 | } 40 | } 41 | 42 | func (c *Client) LoggedIn() bool { 43 | return c.playerToken != nil && c.playerToken.TokenExpiration.Before(time.Now()) 44 | } 45 | 46 | func (c *Client) Login(ctx context.Context) error { 47 | liveToken, err := c.src.Token() 48 | if err != nil { 49 | return err 50 | } 51 | err = c.loginWithXbox(ctx, liveToken) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | err = c.loginMaster(ctx) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (c *Client) loginWithXbox(ctx context.Context, liveToken *oauth2.Token) error { 65 | xboxToken, err := xbox.RequestXBLToken(ctx, liveToken, "rp://playfabapi.com/", &xbox.DeviceTypeAndroid) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | authService, err := c.discovery.AuthService() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | resp, err := doPlayfabRequest[loginResponse](ctx, c.http, authService.PlayfabTitleID, "/Client/LoginWithXbox?sdk="+minecraftDefaultSDK, xboxLoginRequest{ 76 | CreateAccount: true, 77 | InfoRequestParameters: infoRequestParameters{ 78 | PlayerProfile: true, 79 | UserAccountInfo: true, 80 | }, 81 | TitleID: strings.ToUpper(authService.PlayfabTitleID), 82 | XboxToken: fmt.Sprintf("XBL3.0 x=%v;%v", xboxToken.AuthorizationToken.DisplayClaims.UserInfo[0].UserHash, xboxToken.AuthorizationToken.Token), 83 | }, nil) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | c.accountID = resp.Data.PlayFabID 89 | c.sessionTicket = resp.Data.SessionTicket 90 | c.playerToken = &resp.Data.EntityToken 91 | return nil 92 | } 93 | 94 | func (c *Client) loginMaster(ctx context.Context) error { 95 | authService, err := c.discovery.AuthService() 96 | if err != nil { 97 | return err 98 | } 99 | 100 | resp, err := doPlayfabRequest[EntityToken](ctx, c.http, authService.PlayfabTitleID, "/Authentication/GetEntityToken?sdk="+minecraftDefaultSDK, entityTokenRequest{ 101 | Entity: &Entity{ 102 | ID: c.accountID, 103 | Type: "master_player_account", 104 | }, 105 | }, authToken(c.playerToken)) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | c.masterToken = resp.Data 111 | return nil 112 | } 113 | 114 | func doPlayfabRequest[T any](ctx context.Context, client *http.Client, titleID, endpoint string, payload any, token func(*http.Request)) (*Response[T], error) { 115 | body, err := json.Marshal(payload) 116 | if err != nil { 117 | return nil, err 118 | } 119 | req, err := http.NewRequestWithContext(ctx, "POST", "https://"+titleID+".playfabapi.com"+endpoint, bytes.NewReader(body)) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | req.Header.Set("Accept", "application/json") 125 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 126 | req.Header.Set("User-Agent", minecraftUserAgent) 127 | req.Header.Set("X-PlayFabSDK", minecraftDefaultSDK) 128 | req.Header.Set("X-ReportErrorAsSuccess", "true") 129 | if token != nil { 130 | token(req) 131 | } 132 | 133 | res, err := client.Do(req) 134 | if err != nil { 135 | return nil, err 136 | } 137 | defer res.Body.Close() 138 | 139 | if res.StatusCode >= 400 { 140 | playfabErr := PlayfabError{} 141 | err = json.NewDecoder(res.Body).Decode(&playfabErr) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return nil, &playfabErr 146 | } 147 | 148 | var resp Response[T] 149 | err = json.NewDecoder(res.Body).Decode(&resp) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | return &resp, nil 155 | } 156 | 157 | func authToken(token *EntityToken) func(req *http.Request) { 158 | return func(req *http.Request) { 159 | req.Header.Set("X-EntityToken", token.EntityToken) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /utils/playfab/types.go: -------------------------------------------------------------------------------- 1 | //go:build false 2 | 3 | package playfab 4 | 5 | import "time" 6 | 7 | type Response[T any] struct { 8 | Code int `json:"code"` 9 | Status string `json:"string"` 10 | Data *T `json:"data"` 11 | } 12 | 13 | type PlayfabError struct { 14 | StatusCode int `json:"-"` 15 | Status string 16 | ErrorCode int `json:"errorCode"` 17 | ErrorMessage string `json:"errorMessage"` 18 | } 19 | 20 | func (p *PlayfabError) Error() string { 21 | return p.ErrorMessage 22 | } 23 | 24 | type Entity struct { 25 | ID string `json:"Id"` 26 | Type string 27 | TypeString string 28 | } 29 | 30 | type EntityToken struct { 31 | EntityToken string `json:"EntityToken"` 32 | TokenExpiration time.Time `json:"TokenExpiration"` 33 | Entity *Entity `json:"Entity"` 34 | } 35 | 36 | type infoRequestParameters struct { 37 | CharacterInventories bool `json:"GetCharacterInventories"` 38 | CharacterList bool `json:"GetCharacterList"` 39 | PlayerProfile bool `json:"GetPlayerProfile"` 40 | PlayerStatistics bool `json:"GetPlayerStatistics"` 41 | TitleData bool `json:"GetTitleData"` 42 | UserAccountInfo bool `json:"GetUserAccountInfo"` 43 | UserData bool `json:"GetUserData"` 44 | UserInventory bool `json:"GetUserInventory"` 45 | UserReadOnlyData bool `json:"GetUserReadOnlyData"` 46 | UserVirtualCurrency bool `json:"GetUserVirtualCurrency"` 47 | PlayerStatisticNames any `json:"PlayerStatisticNames"` 48 | ProfileConstraints any `json:"ProfileConstraints"` 49 | TitleDataKeys any `json:"TitleDataKeys"` 50 | UserDataKeys any `json:"UserDataKeys"` 51 | UserReadOnlyDataKeys any `json:"UserReadOnlyDataKeys"` 52 | } 53 | 54 | type xboxLoginRequest struct { 55 | CreateAccount bool `json:"CreateAccount"` 56 | EncryptedRequest any `json:"EncryptedRequest"` 57 | InfoRequestParameters infoRequestParameters `json:"InfoRequestParameters"` 58 | PlayerSecret any `json:"PlayerSecret"` 59 | TitleID string `json:"TitleId"` 60 | XboxToken string `json:"XboxToken"` 61 | } 62 | 63 | type loginResponse struct { 64 | SessionTicket string `json:"SessionTicket"` 65 | PlayFabID string `json:"PlayFabId"` 66 | NewlyCreated bool `json:"NewlyCreated"` 67 | SettingsForUser struct { 68 | NeedsAttribution bool `json:"NeedsAttribution"` 69 | GatherDeviceInfo bool `json:"GatherDeviceInfo"` 70 | GatherFocusInfo bool `json:"GatherFocusInfo"` 71 | } `json:"SettingsForUser"` 72 | LastLoginTime time.Time `json:"LastLoginTime"` 73 | InfoResultPayload struct { 74 | AccountInfo struct { 75 | PlayFabID string `json:"PlayFabId"` 76 | Created time.Time `json:"Created"` 77 | TitleInfo struct { 78 | DisplayName string `json:"DisplayName"` 79 | Origination string `json:"Origination"` 80 | Created time.Time `json:"Created"` 81 | LastLogin time.Time `json:"LastLogin"` 82 | FirstLogin time.Time `json:"FirstLogin"` 83 | IsBanned bool `json:"isBanned"` 84 | TitlePlayerAccount struct { 85 | ID string `json:"Id"` 86 | Type string `json:"Type"` 87 | TypeString string `json:"TypeString"` 88 | } `json:"TitlePlayerAccount"` 89 | } `json:"TitleInfo"` 90 | PrivateInfo struct { 91 | } `json:"PrivateInfo"` 92 | XboxInfo struct { 93 | XboxUserID string `json:"XboxUserId"` 94 | XboxUserSandbox string `json:"XboxUserSandbox"` 95 | } `json:"XboxInfo"` 96 | } `json:"AccountInfo"` 97 | UserInventory []any `json:"UserInventory"` 98 | UserDataVersion int `json:"UserDataVersion"` 99 | UserReadOnlyDataVersion int `json:"UserReadOnlyDataVersion"` 100 | CharacterInventories []any `json:"CharacterInventories"` 101 | PlayerProfile struct { 102 | PublisherID string `json:"PublisherId"` 103 | TitleID string `json:"TitleId"` 104 | PlayerID string `json:"PlayerId"` 105 | DisplayName string `json:"DisplayName"` 106 | } `json:"PlayerProfile"` 107 | } `json:"InfoResultPayload"` 108 | EntityToken EntityToken `json:"EntityToken"` 109 | TreatmentAssignment struct { 110 | Variants []any `json:"Variants"` 111 | Variables []any `json:"Variables"` 112 | } `json:"TreatmentAssignment"` 113 | } 114 | 115 | type entityTokenRequest struct { 116 | Entity *Entity `json:"Entity"` 117 | } 118 | -------------------------------------------------------------------------------- /utils/proxy/blobcache/blobcache_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package blobcache 4 | 5 | func checkShouldReadOnly(err error) bool { 6 | return false 7 | } 8 | -------------------------------------------------------------------------------- /utils/proxy/blobcache/blobcache_windows.go: -------------------------------------------------------------------------------- 1 | package blobcache 2 | 3 | import "golang.org/x/sys/windows" 4 | 5 | func checkShouldReadOnly(err error) bool { 6 | return err == windows.ERROR_SHARING_VIOLATION 7 | } 8 | -------------------------------------------------------------------------------- /utils/proxy/context.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io/fs" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | "time" 12 | 13 | "github.com/bedrock-tool/bedrocktool/ui/messages" 14 | "github.com/bedrock-tool/bedrocktool/utils" 15 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 16 | "github.com/sandertv/gophertunnel/minecraft/resource" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | type errTransfer struct { 21 | transfer *packet.Transfer 22 | } 23 | 24 | func (e *errTransfer) Error() string { 25 | return fmt.Sprintf("transfer to %s:%d", e.transfer.Address, e.transfer.Port) 26 | } 27 | 28 | type Context struct { 29 | ctx context.Context 30 | wg sync.WaitGroup 31 | settings ProxySettings 32 | OnPlayerMove []func() 33 | 34 | addedPacks []resource.Pack 35 | handlers []func() *Handler 36 | } 37 | 38 | // New creates a new proxy context 39 | func New(ctx context.Context, settings ProxySettings) (*Context, error) { 40 | p := &Context{ 41 | ctx: ctx, 42 | settings: settings, 43 | } 44 | return p, nil 45 | } 46 | 47 | // AddHandler adds a handler to the proxy 48 | func (p *Context) AddHandler(handler func() *Handler) { 49 | p.handlers = append(p.handlers, handler) 50 | } 51 | 52 | func (p *Context) Context() context.Context { 53 | return p.ctx 54 | } 55 | 56 | func (p *Context) connect(connectInfo *utils.ConnectInfo, withClient bool) (err error) { 57 | session := NewSession(p.ctx, p.settings, p.addedPacks, withClient) 58 | for _, handlerFunc := range p.handlers { 59 | session.handlers = append(session.handlers, handlerFunc()) 60 | } 61 | 62 | serverName, err := connectInfo.Name(p.ctx) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | session.handlers.SessionStart(session, serverName) 68 | err = session.Run(connectInfo) 69 | session.handlers.OnSessionEnd(session, &p.wg) 70 | 71 | if err, ok := err.(*errTransfer); ok { 72 | if connectInfo.IsReplay() { 73 | return nil 74 | } 75 | address := fmt.Sprintf("%s:%d", err.transfer.Address, err.transfer.Port) 76 | logrus.Infof("transferring to %s", address) 77 | return p.connect(&utils.ConnectInfo{ 78 | Value: address, 79 | }, withClient) 80 | } 81 | return err 82 | } 83 | 84 | func (p *Context) newPlayerHandler() *Handler { 85 | return &Handler{ 86 | Name: "Player", 87 | PacketCallback: func(s *Session, pk packet.Packet, toServer bool, timeReceived time.Time, preLogin bool) (packet.Packet, error) { 88 | if pk, ok := pk.(*packet.PacketViolationWarning); ok { 89 | logrus.Infof("%+#v\n", pk) 90 | } 91 | 92 | haveMoved := s.Player.handlePackets(pk) 93 | if haveMoved { 94 | for _, cb := range p.OnPlayerMove { 95 | cb() 96 | } 97 | } 98 | return pk, nil 99 | }, 100 | } 101 | } 102 | 103 | func (p *Context) Run(withClient bool) (err error) { 104 | err = utils.Netisolation() 105 | if err != nil { 106 | logrus.Warnf("Failed to Enable Loopback for Minecraft: %s", err) 107 | } 108 | 109 | defer func() { 110 | messages.SendEvent(&messages.EventSetUIState{ 111 | State: messages.UIStateFinished, 112 | }) 113 | }() 114 | 115 | if p.settings.ConnectInfo == nil || p.settings.ConnectInfo.Value == "" { 116 | return fmt.Errorf("no address") 117 | } 118 | 119 | if p.settings.ConnectInfo.IsReplay() && !utils.Auth.LoggedIn() { 120 | err := <-utils.Auth.RequestLogin() 121 | if err != nil { 122 | return err 123 | } 124 | if !utils.Auth.LoggedIn() { 125 | return errors.New("not Logged In") 126 | } 127 | } 128 | 129 | if p.settings.Capture { 130 | p.AddHandler(NewPacketCapturer) 131 | } 132 | p.AddHandler(p.newPlayerHandler) 133 | p.addedPacks, err = loadForcedPacks() 134 | if err != nil { 135 | return err 136 | } 137 | 138 | err = p.connect(p.settings.ConnectInfo, withClient) 139 | p.wg.Wait() 140 | return err 141 | } 142 | 143 | func loadForcedPacks() ([]resource.Pack, error) { 144 | var packs []resource.Pack 145 | if _, err := os.Stat("forcedpacks"); err == nil { 146 | if err = filepath.WalkDir("forcedpacks/", func(path string, d fs.DirEntry, err error) error { 147 | if err != nil { 148 | return err 149 | } 150 | if d.IsDir() { 151 | return nil 152 | } 153 | ext := filepath.Ext(path) 154 | switch ext { 155 | case ".mcpack", ".zip": 156 | pack, err := resource.ReadPath(path) 157 | if err != nil { 158 | return err 159 | } 160 | packs = append(packs, pack) 161 | logrus.Infof("Added %s to the forced packs", pack.Name()) 162 | default: 163 | logrus.Warnf("Unrecognized file %s in forcedpacks", path) 164 | } 165 | return nil 166 | }); err != nil { 167 | return nil, err 168 | } 169 | } 170 | return packs, nil 171 | } 172 | -------------------------------------------------------------------------------- /utils/proxy/handlers.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "time" 7 | 8 | "github.com/sandertv/gophertunnel/minecraft" 9 | "github.com/sandertv/gophertunnel/minecraft/protocol" 10 | "github.com/sandertv/gophertunnel/minecraft/protocol/login" 11 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 12 | "github.com/sandertv/gophertunnel/minecraft/resource" 13 | ) 14 | 15 | type Handlers []*Handler 16 | 17 | type Handler struct { 18 | Name string 19 | 20 | SessionStart func(s *Session, serverName string) error 21 | PlayerDataModifier func(s *Session, identity *login.IdentityData, data *login.ClientData) 22 | GameDataModifier func(s *Session, gameData *minecraft.GameData) 23 | FilterResourcePack func(s *Session, id string) bool 24 | OnFinishedPack func(s *Session, pack resource.Pack) error 25 | 26 | PacketRaw func(s *Session, header packet.Header, payload []byte, src, dst net.Addr, timeReceived time.Time) 27 | PacketCallback func(s *Session, pk packet.Packet, toServer bool, timeReceived time.Time, preLogin bool) (packet.Packet, error) 28 | 29 | OnServerConnect func(s *Session) (cancel bool, err error) 30 | OnConnect func(s *Session) (cancel bool) 31 | 32 | OnSessionEnd func(s *Session, wg *sync.WaitGroup) 33 | OnBlobs func(s *Session, blobs []protocol.CacheBlob) 34 | } 35 | 36 | func (h Handlers) SessionStart(s *Session, serverName string) error { 37 | for _, handler := range h { 38 | if handler.SessionStart == nil { 39 | continue 40 | } 41 | err := handler.SessionStart(s, serverName) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func (h Handlers) GameDataModifier(s *Session, gameData *minecraft.GameData) { 50 | for _, handler := range h { 51 | if handler.GameDataModifier == nil { 52 | continue 53 | } 54 | handler.GameDataModifier(s, gameData) 55 | } 56 | } 57 | 58 | func (h Handlers) PlayerDataModifier(s *Session, identity *login.IdentityData, data *login.ClientData) { 59 | for _, handler := range h { 60 | if handler.PlayerDataModifier == nil { 61 | continue 62 | } 63 | handler.PlayerDataModifier(s, identity, data) 64 | } 65 | } 66 | 67 | func (h Handlers) FilterResourcePack(s *Session, id string) bool { 68 | for _, handler := range h { 69 | if handler.FilterResourcePack == nil { 70 | continue 71 | } 72 | if handler.FilterResourcePack(s, id) { 73 | return true 74 | } 75 | } 76 | return false 77 | } 78 | 79 | func (h Handlers) OnFinishedPack(s *Session, pack resource.Pack) error { 80 | for _, handler := range h { 81 | if handler.OnFinishedPack == nil { 82 | continue 83 | } 84 | err := handler.OnFinishedPack(s, pack) 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | func (h Handlers) PacketRaw(s *Session, header packet.Header, payload []byte, src, dst net.Addr, timeReceived time.Time) { 93 | for _, handler := range h { 94 | if handler.PacketRaw == nil { 95 | continue 96 | } 97 | handler.PacketRaw(s, header, payload, src, dst, timeReceived) 98 | } 99 | } 100 | func (h Handlers) PacketCallback(s *Session, pk packet.Packet, toServer bool, timeReceived time.Time, preLogin bool) (packet.Packet, error) { 101 | var err error 102 | for _, handler := range h { 103 | if handler.PacketCallback == nil { 104 | continue 105 | } 106 | pk, err = handler.PacketCallback(s, pk, toServer, timeReceived, preLogin) 107 | if err != nil { 108 | return nil, err 109 | } 110 | if pk == nil { 111 | return nil, nil 112 | } 113 | } 114 | return pk, nil 115 | } 116 | 117 | func (h Handlers) OnServerConnect(s *Session) (cancel bool, err error) { 118 | for _, handler := range h { 119 | if handler.OnServerConnect == nil { 120 | continue 121 | } 122 | cancel, err = handler.OnServerConnect(s) 123 | if err != nil { 124 | return false, err 125 | } 126 | if cancel { 127 | return true, nil 128 | } 129 | } 130 | return false, nil 131 | } 132 | 133 | func (h Handlers) OnConnect(s *Session) (cancel bool) { 134 | for _, handler := range h { 135 | if handler.OnConnect == nil { 136 | continue 137 | } 138 | if handler.OnConnect(s) { 139 | return true 140 | } 141 | } 142 | return false 143 | } 144 | 145 | func (h Handlers) OnSessionEnd(s *Session, wg *sync.WaitGroup) { 146 | for _, handler := range h { 147 | if handler.OnSessionEnd == nil { 148 | continue 149 | } 150 | handler.OnSessionEnd(s, wg) 151 | } 152 | } 153 | 154 | func (h Handlers) OnBlobs(s *Session, blobs []protocol.CacheBlob) { 155 | for _, handler := range h { 156 | if handler.OnBlobs == nil { 157 | continue 158 | } 159 | handler.OnBlobs(s, blobs) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /utils/proxy/packet_logger.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "reflect" 7 | "sync" 8 | "time" 9 | 10 | "github.com/bedrock-tool/bedrocktool/utils" 11 | "github.com/fatih/color" 12 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 13 | "github.com/sirupsen/logrus" 14 | "golang.org/x/exp/slices" 15 | ) 16 | 17 | var mutedPackets = []uint32{ 18 | packet.IDUpdateBlock, 19 | packet.IDMoveActorAbsolute, 20 | packet.IDSetActorMotion, 21 | packet.IDSetTime, 22 | packet.IDRemoveActor, 23 | packet.IDAddActor, 24 | packet.IDUpdateAttributes, 25 | packet.IDInteract, 26 | packet.IDLevelEvent, 27 | packet.IDSetActorData, 28 | packet.IDMoveActorDelta, 29 | packet.IDMovePlayer, 30 | packet.IDBlockActorData, 31 | packet.IDPlayerAuthInput, 32 | packet.IDLevelChunk, 33 | packet.IDLevelSoundEvent, 34 | packet.IDActorEvent, 35 | packet.IDNetworkChunkPublisherUpdate, 36 | packet.IDUpdateSubChunkBlocks, 37 | packet.IDSubChunk, 38 | packet.IDSubChunkRequest, 39 | packet.IDAnimate, 40 | packet.IDNetworkStackLatency, 41 | packet.IDInventoryTransaction, 42 | packet.IDPlaySound, 43 | packet.IDPlayerAction, 44 | packet.IDSetTitle, 45 | packet.IDClientCacheMissResponse, 46 | packet.IDClientCacheBlobStatus, 47 | packet.IDSetScore, 48 | packet.IDMobEquipment, 49 | packet.IDSpawnParticleEffect, 50 | packet.IDAnimateEntity, 51 | packet.IDMobArmourEquipment, 52 | packet.IDMobEffect, 53 | } 54 | 55 | var dirS2C = color.GreenString("S") + "->" + color.CyanString("C") 56 | var dirC2S = color.CyanString("C") + "->" + color.GreenString("S") 57 | 58 | type packetLogger struct { 59 | dumpLock sync.Mutex 60 | timeStart time.Time 61 | packetLogWriter *bufio.Writer 62 | closePacketLog func() error 63 | clientSide bool 64 | } 65 | 66 | func (p *packetLogger) PacketSend(pk packet.Packet, t time.Time) error { 67 | p.dumpLock.Lock() 68 | defer p.dumpLock.Unlock() 69 | return p.logPacket(pk, t, false) 70 | } 71 | 72 | func (p *packetLogger) PacketReceive(pk packet.Packet, t time.Time) error { 73 | p.dumpLock.Lock() 74 | defer p.dumpLock.Unlock() 75 | return p.logPacket(pk, t, false) 76 | } 77 | 78 | func (p *packetLogger) logPacket(pk packet.Packet, t time.Time, toServer bool) error { 79 | if p.packetLogWriter != nil { 80 | if p.timeStart.IsZero() { 81 | p.timeStart = t 82 | } 83 | p.packetLogWriter.WriteString(t.Sub(p.timeStart).Truncate(time.Millisecond).String() + "\n") 84 | utils.DumpStruct(p.packetLogWriter, pk) 85 | p.packetLogWriter.Write([]byte("\n\n\n")) 86 | p.packetLogWriter.Flush() 87 | } 88 | 89 | var dir string = dirS2C 90 | if toServer { 91 | dir = dirC2S 92 | } 93 | 94 | if !p.clientSide && !slices.Contains(mutedPackets, pk.ID()) { 95 | pkName := reflect.TypeOf(pk).String()[8:] 96 | logrus.Debugf("%s %s", dir, pkName) 97 | } 98 | return nil 99 | } 100 | 101 | func (p *packetLogger) Close() error { 102 | if p.packetLogWriter != nil { 103 | p.packetLogWriter.Flush() 104 | return p.closePacketLog() 105 | } 106 | return nil 107 | } 108 | 109 | func NewPacketLogger(verbose, clientSide bool) (*packetLogger, error) { 110 | p := &packetLogger{ 111 | clientSide: clientSide, 112 | } 113 | if verbose { 114 | var logName = "packets.log" 115 | if clientSide { 116 | logName = "packets-client.log" 117 | } 118 | f, err := os.Create(utils.PathData(logName)) 119 | if err != nil { 120 | return nil, err 121 | } 122 | p.packetLogWriter = bufio.NewWriter(f) 123 | p.closePacketLog = f.Close 124 | } 125 | return p, nil 126 | } 127 | -------------------------------------------------------------------------------- /utils/proxy/player.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/go-gl/mathgl/mgl32" 5 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 6 | ) 7 | 8 | type Player struct { 9 | RuntimeID uint64 10 | Position mgl32.Vec3 11 | Pitch, Yaw, HeadYaw float32 12 | } 13 | 14 | func (p *Player) handlePackets(pk packet.Packet) bool { 15 | switch pk := pk.(type) { 16 | case *packet.StartGame: 17 | p.RuntimeID = pk.EntityRuntimeID 18 | case *packet.MovePlayer: 19 | if pk.EntityRuntimeID == p.RuntimeID { 20 | p.Position = pk.Position 21 | p.Pitch = pk.Pitch 22 | p.Yaw = pk.Yaw 23 | p.HeadYaw = pk.HeadYaw 24 | return true 25 | } 26 | case *packet.PlayerAuthInput: 27 | p.Position = pk.Position 28 | p.Pitch = pk.Pitch 29 | p.Yaw = pk.Yaw 30 | p.HeadYaw = pk.HeadYaw 31 | return true 32 | } 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /utils/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "github.com/bedrock-tool/bedrocktool/utils" 11 | "github.com/sandertv/gophertunnel/minecraft/protocol" 12 | "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type ProxySettings struct { 17 | ConnectInfo *utils.ConnectInfo `opt:"Address" flag:"address" desc:"locale.remote_address"` 18 | 19 | Debug bool `opt:"Debug" flag:"debug" desc:"locale.debug_mode"` 20 | ExtraDebug bool `opt:"Extra Debug" flag:"extra-debug" desc:"extra debug info (packet.log)"` 21 | Capture bool `opt:"Packet Capture" flag:"capture" desc:"Capture pcap2 file"` 22 | ClientCache bool `opt:"Client Cache" flag:"client-cache" default:"true" desc:"Enable Client Cache"` 23 | ListenAddress string `opt:"Listen Address" flag:"listen" default:"0.0.0.0:19132" desc:"example :19132 or 127.0.0.1:19132"` 24 | } 25 | 26 | type PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr, timeReceived time.Time) 27 | type ingameCommand struct { 28 | Exec func(cmdline []string) bool 29 | Cmd protocol.Command 30 | } 31 | 32 | var NewPacketCapturer func() *Handler 33 | 34 | var errCancelConnect = fmt.Errorf("cancelled connecting") 35 | 36 | var serverPool = packet.NewServerPool() 37 | var clientPool = packet.NewClientPool() 38 | 39 | func DecodePacket(header packet.Header, payload []byte, shieldID int32) (pk packet.Packet, ok bool) { 40 | pkFunc, ok := serverPool[header.PacketID] 41 | if !ok { 42 | pkFunc, ok = clientPool[header.PacketID] 43 | } 44 | if ok { 45 | pk = pkFunc() 46 | } else { 47 | pk = &packet.Unknown{PacketID: header.PacketID, Payload: payload} 48 | } 49 | 50 | ok = true 51 | defer func() { 52 | if recoveredErr := recover(); recoveredErr != nil { 53 | logrus.Errorf("%T: %s", pk, recoveredErr.(error)) 54 | logrus.Debugf("payload: %s", hex.EncodeToString(payload)) 55 | ok = false 56 | } 57 | }() 58 | pk.Marshal(protocol.NewReader(bytes.NewBuffer(payload), shieldID, false)) 59 | return pk, ok 60 | } 61 | -------------------------------------------------------------------------------- /utils/proxy/resourcepacks/packcache.go: -------------------------------------------------------------------------------- 1 | package resourcepacks 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/bedrock-tool/bedrocktool/utils" 8 | "github.com/sandertv/gophertunnel/minecraft/resource" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type PackCache interface { 14 | Get(id uuid.UUID, ver string) (resource.Pack, error) 15 | Has(id uuid.UUID, ver string) bool 16 | Create(id uuid.UUID, ver string) (*closeMoveWriter, error) 17 | } 18 | 19 | type packCache struct { 20 | Ignore bool 21 | } 22 | 23 | func (packCache) cachedPath(id uuid.UUID, ver string) string { 24 | return utils.PathCache("packcache", id.String()+"_"+ver+".zip") 25 | } 26 | 27 | func (c *packCache) Get(id uuid.UUID, ver string) (resource.Pack, error) { 28 | if c.Ignore { 29 | panic("not allowed") 30 | } 31 | f, err := utils.OpenShared(c.cachedPath(id, ver)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | stat, _ := f.Stat() 36 | return resource.FromReaderAt(f, stat.Size()) 37 | } 38 | 39 | func (c *packCache) Has(id uuid.UUID, ver string) bool { 40 | if c.Ignore { 41 | return false 42 | } 43 | _, err := os.Stat(c.cachedPath(id, ver)) 44 | return err == nil 45 | } 46 | 47 | func (c *packCache) Create(id uuid.UUID, ver string) (*closeMoveWriter, error) { 48 | if c.Ignore { 49 | return nil, nil 50 | } 51 | 52 | finalPath := c.cachedPath(id, ver) 53 | tmpPath := finalPath + ".tmp" 54 | 55 | _ = os.MkdirAll(filepath.Dir(finalPath), 0777) 56 | 57 | f, err := utils.CreateShared(tmpPath) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return &closeMoveWriter{ 63 | File: f, 64 | FinalName: finalPath, 65 | }, nil 66 | } 67 | 68 | type closeMoveWriter struct { 69 | *os.File 70 | FinalName string 71 | } 72 | 73 | func (c *closeMoveWriter) Move() error { 74 | return os.Rename(c.File.Name(), c.FinalName) 75 | } 76 | -------------------------------------------------------------------------------- /utils/proxy/resourcepacks/replaycache.go: -------------------------------------------------------------------------------- 1 | package resourcepacks 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "io" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/google/uuid" 11 | "github.com/sandertv/gophertunnel/minecraft/resource" 12 | ) 13 | 14 | type ReplayCache struct { 15 | packs map[string]resource.Pack 16 | } 17 | 18 | func NewReplayCache() *ReplayCache { 19 | return &ReplayCache{} 20 | } 21 | 22 | func (r *ReplayCache) Get(id uuid.UUID, ver string) (resource.Pack, error) { 23 | return r.packs[id.String()+"_"+ver], nil 24 | } 25 | 26 | func (r *ReplayCache) Has(id uuid.UUID, ver string) bool { 27 | _, ok := r.packs[id.String()+"_"+ver] 28 | return ok 29 | } 30 | 31 | func (r *ReplayCache) Create(id uuid.UUID, ver string) (*closeMoveWriter, error) { return nil, nil } 32 | 33 | func (r *ReplayCache) ReadFrom(reader io.ReaderAt, readerSize int64) error { 34 | z, err := zip.NewReader(reader, readerSize) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | r.packs = make(map[string]resource.Pack) 40 | for _, f := range z.File { 41 | f.Name = strings.ReplaceAll(f.Name, "\\", "/") 42 | if filepath.Dir(f.Name) == "packcache" { 43 | if f.Method != zip.Store { 44 | return errors.New("packcache compressed") 45 | } 46 | offset, err := f.DataOffset() 47 | if err != nil { 48 | return err 49 | } 50 | packReader := io.NewSectionReader(reader, offset, int64(f.CompressedSize64)) 51 | pack, err := resource.FromReaderAt(packReader, int64(f.CompressedSize64)) 52 | if err != nil { 53 | return err 54 | } 55 | r.packs[pack.UUID().String()+"_"+pack.Version()] = pack 56 | } 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /utils/recover.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/sirupsen/logrus" 4 | 5 | var ErrorHandler = func(err error) { 6 | logrus.Fatal(err) 7 | } 8 | 9 | func RecoverCall(f func() error) (err error) { 10 | defer func() { 11 | if errr, ok := recover().(error); ok { 12 | err = errr 13 | } 14 | }() 15 | err = f() 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /utils/rmtree.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | func walkDirRemove(name string) error { 9 | f, err := os.Open(name) 10 | if err != nil { 11 | return err 12 | } 13 | names, err := f.Readdirnames(-1) 14 | f.Close() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | for _, name1 := range names { 20 | name2 := path.Join(name, name1) 21 | err := RemoveFile(name2) 22 | if err != nil { 23 | if err := walkDirRemove(name2); err != nil { 24 | return err 25 | } 26 | err = RemoveDir(name2) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func RemoveTree(dir string) error { 37 | return walkDirRemove(dir) 38 | } 39 | -------------------------------------------------------------------------------- /utils/rmtree_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package utils 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func RemoveDir(name string) error { 10 | return syscall.Rmdir(name) 11 | } 12 | 13 | func RemoveFile(name string) error { 14 | return syscall.Unlink(name) 15 | } 16 | -------------------------------------------------------------------------------- /utils/rmtree_windows.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | ) 7 | 8 | func RemoveDir(name string) error { 9 | p, e := syscall.UTF16PtrFromString(name) 10 | if e != nil { 11 | return &os.PathError{Op: "remove", Path: name, Err: e} 12 | } 13 | return syscall.RemoveDirectory(p) 14 | } 15 | 16 | func RemoveFile(name string) error { 17 | p, e := syscall.UTF16PtrFromString(name) 18 | if e != nil { 19 | return &os.PathError{Op: "remove", Path: name, Err: e} 20 | } 21 | return syscall.DeleteFile(p) 22 | } 23 | -------------------------------------------------------------------------------- /utils/stripansi.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" 8 | 9 | var re = regexp.MustCompile(ansi) 10 | 11 | func StripAnsi(str string) string { 12 | return re.ReplaceAllString(str, "") 13 | } 14 | 15 | func StripAnsiBytes(str []byte) []byte { 16 | return re.ReplaceAll(str, nil) 17 | } 18 | -------------------------------------------------------------------------------- /utils/temp_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package utils 4 | 5 | import "os" 6 | 7 | func CreateShared(name string) (*os.File, error) { 8 | return os.Create(name) 9 | } 10 | 11 | func OpenShared(name string) (*os.File, error) { 12 | return os.Open(name) 13 | } 14 | -------------------------------------------------------------------------------- /utils/temp_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package utils 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | func CreateShared(name string) (*os.File, error) { 13 | path, _ := syscall.UTF16PtrFromString(name) 14 | hand, err := windows.CreateFile(path, 15 | syscall.GENERIC_READ|syscall.GENERIC_WRITE, 16 | syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_READ|syscall.FILE_SHARE_DELETE, 17 | nil, 18 | syscall.CREATE_ALWAYS, 19 | syscall.FILE_ATTRIBUTE_NORMAL, 20 | 0, 21 | ) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return os.NewFile(uintptr(hand), name), nil 26 | } 27 | 28 | func OpenShared(name string) (*os.File, error) { 29 | path, _ := syscall.UTF16PtrFromString(name) 30 | hand, err := windows.CreateFile(path, 31 | syscall.GENERIC_READ, 32 | syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_READ|syscall.FILE_SHARE_DELETE, 33 | nil, 34 | syscall.OPEN_EXISTING, 35 | syscall.FILE_ATTRIBUTE_NORMAL, 36 | 0, 37 | ) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return os.NewFile(uintptr(hand), name), nil 42 | } 43 | -------------------------------------------------------------------------------- /utils/texture_map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "image/png" 7 | "os" 8 | 9 | "github.com/OneOfOne/xxhash" 10 | "github.com/df-mc/dragonfly/server/world" 11 | ) 12 | 13 | type TextureMap struct { 14 | BlockSize int 15 | Lookup *image.RGBA 16 | } 17 | 18 | func NewTextureMap() *TextureMap { 19 | return &TextureMap{ 20 | BlockSize: 16, 21 | } 22 | } 23 | 24 | type BlockRID = uint32 25 | type TexMapIdx = uint32 26 | type TexMapHash = uint64 27 | type TexMapEntry struct { 28 | X uint16 29 | Y uint16 30 | Transparent bool 31 | } 32 | 33 | func hashImage(img image.Image) uint64 { 34 | h := xxhash.New64() 35 | switch img := img.(type) { 36 | case *image.RGBA: 37 | h.Write(img.Pix) 38 | case *image.Paletted: 39 | h.Write(img.Pix) 40 | } 41 | return h.Sum64() 42 | } 43 | 44 | func (t *TextureMap) SetTextures(blocks []world.Block, resolvedTextures map[string]image.Image) map[BlockRID]TexMapEntry { 45 | // resolvedTextures = map from block name -> block top texture 46 | 47 | var hashes = map[TexMapHash]image.Image{} 48 | var hashToRids = map[TexMapHash][]BlockRID{} 49 | var ridToIdx = map[BlockRID]TexMapEntry{} 50 | 51 | for rid, block := range blocks { 52 | name, _ := block.EncodeBlock() 53 | tex, ok := resolvedTextures[name] 54 | if ok { 55 | h := hashImage(tex) 56 | hashToRids[h] = append(hashToRids[h], uint32(rid)) 57 | hashes[h] = tex 58 | } 59 | } 60 | 61 | t.Lookup = image.NewRGBA(image.Rect(0, 0, 1024, 512)) 62 | i := 0 63 | for k, he := range hashToRids { 64 | tex := hashes[k] 65 | 66 | x := (i * t.BlockSize) % t.Lookup.Rect.Dx() 67 | y := (i * t.BlockSize) / t.Lookup.Rect.Dx() * t.BlockSize 68 | draw.Draw(t.Lookup, image.Rect(x, y, x+t.BlockSize, y+t.BlockSize), tex, image.Point{}, draw.Over) 69 | 70 | for _, v := range he { 71 | ridToIdx[v] = TexMapEntry{ 72 | X: uint16(x), 73 | Y: uint16(y), 74 | Transparent: false, 75 | } 76 | } 77 | 78 | i++ 79 | } 80 | 81 | if IsDebug() { 82 | f, err := os.Create("tex.png") 83 | if err != nil { 84 | panic(err) 85 | } 86 | png.Encode(f, t.Lookup) 87 | f.Close() 88 | } 89 | 90 | return ridToIdx 91 | } 92 | -------------------------------------------------------------------------------- /utils/updater/updater_android.go: -------------------------------------------------------------------------------- 1 | package updater 2 | -------------------------------------------------------------------------------- /utils/updater/updater_other.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | 3 | package updater 4 | 5 | import ( 6 | "compress/gzip" 7 | "crypto" 8 | "encoding/base64" 9 | "fmt" 10 | "runtime" 11 | 12 | "github.com/bedrock-tool/bedrocktool/utils" 13 | "github.com/minio/selfupdate" 14 | ) 15 | 16 | func DoUpdate() error { 17 | update, err := UpdateAvailable() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | checksum, err := base64.StdEncoding.DecodeString(update.Sha256) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | r, _, err := fetchHttp(fmt.Sprintf("%s%s/%s/%s-%s.gz", UpdateServer, utils.CmdName, update.Version, runtime.GOOS, runtime.GOARCH)) 28 | if err != nil { 29 | return err 30 | } 31 | gr, err := gzip.NewReader(r) 32 | if err != nil { 33 | return err 34 | } 35 | defer gr.Close() 36 | 37 | err = selfupdate.Apply(gr, selfupdate.Options{ 38 | Checksum: checksum, 39 | Hash: crypto.SHA256, 40 | }) 41 | if err != nil { 42 | return err 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /utils/ver.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | var Version string 4 | var CmdName = "invalid" 5 | 6 | func IsDebug() bool { 7 | return Version == "" 8 | } 9 | -------------------------------------------------------------------------------- /utils/xbox/device_token.go: -------------------------------------------------------------------------------- 1 | package xbox 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ecdsa" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "net/http" 12 | "strings" 13 | 14 | "github.com/google/uuid" 15 | ) 16 | 17 | type DeviceType struct { 18 | DeviceType string 19 | ClientID string 20 | TitleID string 21 | Version string 22 | UserAgent string 23 | } 24 | 25 | var ( 26 | DeviceTypeAndroid = DeviceType{ 27 | DeviceType: "Android", 28 | ClientID: "0000000048183522", 29 | TitleID: "1739947436", 30 | Version: "8.0.0", 31 | UserAgent: "XAL Android 2020.07.20200714.000", 32 | } 33 | DeviceTypeIOS = DeviceType{ 34 | DeviceType: "iOS", 35 | ClientID: "000000004c17c01a", 36 | TitleID: "1810924247", 37 | Version: "15.6.1", 38 | UserAgent: "XAL iOS 2021.11.20211021.000", 39 | } 40 | DeviceTypeWindows = DeviceType{ 41 | DeviceType: "Win32", 42 | ClientID: "0000000040159362", 43 | TitleID: "896928775", 44 | Version: "10.0.25398.4909", 45 | UserAgent: "XAL Win32 2021.11.20220411.002", 46 | } 47 | DeviceTypeNintendo = DeviceType{ 48 | DeviceType: "Nintendo", 49 | ClientID: "00000000441cc96b", 50 | TitleID: "2047319603", 51 | Version: "0.0.0", 52 | UserAgent: "XAL", 53 | } 54 | DeviceTypePlaystation = DeviceType{ 55 | DeviceType: "Playstation", 56 | ClientID: "000000004827c78e", 57 | TitleID: "idk", 58 | Version: "10.0.0", 59 | UserAgent: "XAL", 60 | } 61 | ) 62 | 63 | // deviceToken is the token obtained by requesting a device token by posting to xblDeviceAuthURL. Its Token 64 | // field may be used in a request to obtain the XSTS token. 65 | type deviceToken struct { 66 | Token string 67 | } 68 | 69 | // obtainDeviceToken sends a POST request to the device auth endpoint using the ECDSA private key passed to 70 | // sign the request. 71 | func obtainDeviceToken(ctx context.Context, c *http.Client, key *ecdsa.PrivateKey, deviceType *DeviceType) (token *deviceToken, err error) { 72 | var properties = map[string]any{ 73 | "AuthMethod": "ProofOfPossession", 74 | "Id": "", 75 | "DeviceType": deviceType.DeviceType, 76 | "Version": deviceType.Version, 77 | "ProofKey": map[string]any{ 78 | "crv": "P-256", 79 | "alg": "ES256", 80 | "use": "sig", 81 | "kty": "EC", 82 | "x": base64.RawURLEncoding.EncodeToString(key.PublicKey.X.Bytes()), 83 | "y": base64.RawURLEncoding.EncodeToString(key.PublicKey.Y.Bytes()), 84 | }, 85 | } 86 | 87 | switch deviceType.DeviceType { 88 | case "Android", "Nintendo": 89 | properties["Id"] = "{" + uuid.NewString() + "}" 90 | case "iOS": 91 | properties["Id"] = strings.ToUpper(uuid.NewString()) 92 | case "Playstation": 93 | properties["Id"] = uuid.NewString() 94 | case "Win32", "Xbox": 95 | properties["Id"] = "{" + strings.ToUpper(uuid.NewString()) + "}" 96 | properties["SerialNumber"] = properties["Id"] 97 | default: 98 | return nil, errors.New("unknown device type") 99 | } 100 | 101 | data, _ := json.Marshal(map[string]any{ 102 | "RelyingParty": "http://auth.xboxlive.com", 103 | "TokenType": "JWT", 104 | "Properties": properties, 105 | }) 106 | request, _ := http.NewRequestWithContext(ctx, "POST", "https://device.auth.xboxlive.com/device/authenticate", bytes.NewReader(data)) 107 | request.Header.Set("X-Xbl-contract-version", "1") 108 | request.Header.Set("User-Agent", deviceType.UserAgent) 109 | request.Header.Set("Content-Type", "application/json;charset=utf-8") 110 | request.Header.Set("Accept", "application/json") 111 | request.Header.Set("Pragma", "no-cache") 112 | request.Header.Set("Cache-Control", "no-store, must-revalidate, no-cache") 113 | request.Header.Set("Accept-Encoding", "gzip, deflate, compress") 114 | request.Header.Set("Accept-Language", "en-US, en;q=0.9") 115 | sign(request, data, key) 116 | 117 | resp, err := c.Do(request) 118 | if err != nil { 119 | return nil, fmt.Errorf("POST %v: %w", "https://device.auth.xboxlive.com/device/authenticate", err) 120 | } 121 | defer func() { 122 | _ = resp.Body.Close() 123 | }() 124 | if resp.StatusCode != 200 { 125 | return nil, fmt.Errorf("POST %v: %v", "https://device.auth.xboxlive.com/device/authenticate", resp.Status) 126 | } 127 | token = &deviceToken{} 128 | return token, json.NewDecoder(resp.Body).Decode(token) 129 | } 130 | -------------------------------------------------------------------------------- /utils/xbox/sign.go: -------------------------------------------------------------------------------- 1 | package xbox 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "encoding/binary" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | // sign signs the request passed containing the body passed. It signs the request using the ECDSA private key 15 | // passed. If the request has a 'ProofKey' field in the Properties field, that key must be passed here. 16 | func sign(request *http.Request, body []byte, key *ecdsa.PrivateKey) { 17 | currentTime := windowsTimestamp() 18 | hash := sha256.New() 19 | 20 | // Signature policy version (0, 0, 0, 1) + 0 byte. 21 | buf := bytes.NewBuffer([]byte{0, 0, 0, 1, 0}) 22 | // Timestamp + 0 byte. 23 | _ = binary.Write(buf, binary.BigEndian, currentTime) 24 | buf.Write([]byte{0}) 25 | hash.Write(buf.Bytes()) 26 | 27 | // HTTP method, generally POST + 0 byte. 28 | hash.Write([]byte("POST")) 29 | hash.Write([]byte{0}) 30 | // Request uri path + raw query + 0 byte. 31 | hash.Write([]byte(request.URL.Path + request.URL.RawQuery)) 32 | hash.Write([]byte{0}) 33 | 34 | // Authorization header if present, otherwise an empty string + 0 byte. 35 | hash.Write([]byte(request.Header.Get("Authorization"))) 36 | hash.Write([]byte{0}) 37 | 38 | // Body data (only up to a certain limit, but this limit is practically never reached) + 0 byte. 39 | hash.Write(body) 40 | hash.Write([]byte{0}) 41 | 42 | // Sign the checksum produced, and combine the 'r' and 's' into a single signature. 43 | r, s, _ := ecdsa.Sign(rand.Reader, key, hash.Sum(nil)) 44 | signature := append(r.Bytes(), s.Bytes()...) 45 | 46 | // The signature begins with 12 bytes, the first being the signature policy version (0, 0, 0, 1) again, 47 | // and the other 8 the timestamp again. 48 | buf = bytes.NewBuffer([]byte{0, 0, 0, 1}) 49 | _ = binary.Write(buf, binary.BigEndian, currentTime) 50 | 51 | // Append the signature to the other 12 bytes, and encode the signature with standard base64 encoding. 52 | sig := append(buf.Bytes(), signature...) 53 | request.Header.Set("Signature", base64.StdEncoding.EncodeToString(sig)) 54 | } 55 | 56 | // windowsTimestamp returns a Windows specific timestamp. It has a certain offset from Unix time which must be 57 | // accounted for. 58 | func windowsTimestamp() int64 { 59 | return (time.Now().Unix() + 11644473600) * 10000000 60 | } 61 | --------------------------------------------------------------------------------