├── .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 |
--------------------------------------------------------------------------------