Compare commits

...

146 Commits

Author SHA1 Message Date
olebeck e13856a4ff show world download list 2023-04-30 23:04:41 +02:00
olebeck 2761cf094c fix block entities 2023-04-30 19:35:18 +02:00
olebeck 0eb3d09046 1.19.80 2023-04-29 20:57:22 +02:00
olebeck 4968181f0e add buttons to packs 2023-04-29 18:17:24 +02:00
olebeck bbf7cc1d75 update dragonfly 2023-04-29 17:22:24 +02:00
olebeck b867221a66 add progress 2023-04-29 17:12:13 +02:00
olebeck 3cc13a7bbf move some stuff, add packs ui 2023-04-27 21:45:04 +02:00
olebeck ba7435c822 store blocknbt per block not in subchunks 2023-04-21 03:20:57 +02:00
olebeck 5a550002d3 add it to gui 2023-04-21 00:58:20 +02:00
olebeck 63b94197ec add -capture for all commands 2023-04-21 00:55:52 +02:00
olebeck 5149487c18 rewrite replay to support resourcepacks 2023-04-17 21:48:12 +02:00
olebeck 87cd32e280 small cleanup 2023-04-12 14:10:45 +02:00
olebeck aa73c86d11 code cleaning, remove dns 2023-04-10 19:55:08 +02:00
olebeck c20e017f0d fix packs with same names causing issues 2023-04-07 17:40:29 +02:00
olebeck 2c87715966 fix bug with extra debug 2023-04-07 16:35:36 +02:00
olebeck 7726c707c6 small fixes 2023-04-07 16:24:38 +02:00
olebeck 2e920a7bf0 fix shadows on entities 2023-04-05 16:07:49 +02:00
olebeck 796a846b8e settings in gui wrong 2023-04-05 00:03:03 +02:00
olebeck 99f7b012bf add flags for inventory and entity 2023-04-04 20:06:05 +02:00
olebeck 3b1aa74100 fix blocks not showing up in creative menu 2023-04-04 19:38:11 +02:00
olebeck 3dadabf706 update nbtconv 2023-04-04 17:57:24 +02:00
olebeck a5f77eca3c fix item metadata being written to the wrong key 2023-04-04 17:12:21 +02:00
olebeck 2996a752fc settings not properly applied 2023-04-04 16:25:19 +02:00
olebeck 085244e656 fix an issue with resourcepack names 2023-04-04 16:02:12 +02:00
olebeck 543e1335ee fix nil 2023-04-04 04:09:01 +02:00
olebeck 9a677feeac fix debug 2023-04-04 03:57:01 +02:00
olebeck 32bfdfa319 fix multi worlds 2023-04-04 03:51:13 +02:00
olebeck bf786b1010 add grab cursor 2023-04-04 03:27:55 +02:00
olebeck e8dae60049 fix renames 2023-04-04 02:47:25 +02:00
olebeck 17d86c9ddb move handlers 2023-04-04 02:44:13 +02:00
olebeck 4e7cfeb3b4 make worlds a proper handler 2023-04-04 02:42:17 +02:00
olebeck f1cb7df05e make subcommands more modular 2023-04-02 16:49:24 +02:00
olebeck 9b3a31879a small fixes 2023-04-02 14:20:50 +02:00
olebeck 88589933ea format a bit 2023-04-02 00:22:50 +02:00
olebeck 29417b600c go.sum 2023-04-01 21:18:44 +02:00
olebeck eca888e3f2 fix regression on legacy 2023-04-01 21:18:40 +02:00
olebeck 4c0dd665f1 fix out of bounds in inventory 2023-03-31 15:54:25 +02:00
olebeck b7ad339a64 async update check 2023-03-30 13:54:11 +02:00
olebeck 191f1ecd9a go.sum 2023-03-30 13:48:28 +02:00
olebeck 1e990d7fcb performance optimizations 2023-03-30 13:48:03 +02:00
olebeck d3a8219d99 add player inventory 2023-03-28 15:22:11 +02:00
olebeck 94e02d677c checkerboard chunk render 2023-03-27 14:11:40 +02:00
olebeck c5e7146f24 fix gui build failing 2023-03-26 13:06:40 +02:00
olebeck cdf7caea5b fix a revert 2023-03-26 12:45:37 +02:00
olebeck 47fd5cc2ec fix some wrong map rendering 2023-03-25 23:04:53 +01:00
olebeck cef4a5dbfe fix 2023-03-25 22:50:17 +01:00
olebeck 6376195732 fix bugs with worlds on vanilla server 2023-03-25 22:19:14 +01:00
olebeck 1b1bddc17f ignore skin id changes without new data 2023-03-24 23:29:53 +01:00
olebeck ae496e9238 save more entity data 2023-03-24 20:45:53 +01:00
olebeck d7b559a7a8 1.19.70 2023-03-23 20:39:47 +01:00
olebeck 75a3ad3674 wrong water render 2023-03-21 12:34:20 +01:00
olebeck b98b509907 slabs are now treated as solid 2023-03-21 12:15:58 +01:00
olebeck ed910330e8 nearest neighbor on the map 2023-03-21 12:03:11 +01:00
olebeck 5742a6b850 Merge branch 'master' into 1.19.63 2023-03-21 11:22:51 +01:00
olebeck 21566df29f clean 2023-03-21 11:22:07 +01:00
olebeck 68890eddd2 proper zoom 2023-03-21 11:21:31 +01:00
olebeck b21ed74bea basic skin ui 2023-03-19 23:34:35 +01:00
olebeck 75956eabcb bring back super hacky cubecraft "fix" 2023-03-19 02:06:37 +01:00
olebeck 8f88c7f576 Merge branch 'master' into 1.19.63 fix entities falling 2023-03-19 01:45:58 +01:00
olebeck 82de60369a remove has Gravity again 2023-03-19 01:45:27 +01:00
olebeck 83a38c57cc Merge branch 'master' into 1.19.63
dont delete entities
2023-03-19 01:10:36 +01:00
olebeck 073407346e add go.sum 2023-03-19 01:09:02 +01:00
olebeck a2653cffad ignore entitiy remove 2023-03-18 22:30:44 +01:00
olebeck d10805f4eb back to 1.19.63 2023-03-18 12:18:49 +01:00
olebeck 7a6061dba2 Revert "revert to 1.19.63"
This reverts commit 3bf371d07c.
2023-03-18 12:17:20 +01:00
olebeck 3bf371d07c revert to 1.19.63 2023-03-18 12:15:00 +01:00
olebeck ced2b28d9b clean up 2023-03-18 12:12:54 +01:00
olebeck c8aa5389c1 add markVariant 2023-03-15 19:33:05 +01:00
olebeck 525a5e64fe fix wrong update path 2023-03-15 16:14:57 +01:00
olebeck 4cc99f5753 update dragonfly 2023-03-15 15:58:58 +01:00
olebeck 3bba95dc64 voidGen default in gui 2023-03-15 15:36:07 +01:00
olebeck 105a318aa8 fix world resourcepacks not applying 2023-03-15 15:34:18 +01:00
olebeck 7ddec1b09d rescaled icon, fix app exit 2023-03-15 15:01:34 +01:00
olebeck 3c87663d02 add icon, fix bug in dark theme 2023-03-15 14:51:31 +01:00
olebeck 04293a74c5 properly apply entity scale 2023-03-15 12:51:30 +01:00
olebeck a746dee75b add dark palette 2023-03-15 02:36:55 +01:00
olebeck 359ad83fc0 1.19.70 2023-03-15 01:29:43 +01:00
olebeck 3fbfbcf2fc add a basic zoom and pan 2023-03-14 16:32:18 +01:00
olebeck e51bcc6b7f go is picky 2023-03-14 02:36:54 +01:00
olebeck 31e6daba3a fix 2023-03-14 02:35:44 +01:00
olebeck 710723aedb missing linux deps 2023-03-14 02:14:28 +01:00
olebeck bd8e3fcddb fix compile errors 2023-03-14 02:10:16 +01:00
olebeck a6488c98c9 gui rewrite, enable entities 2023-03-14 02:07:39 +01:00
olebeck a163c6757c improve verbose logger 2023-03-11 19:50:13 +01:00
olebeck 049a9a6d48 add blind proxy 2023-03-11 17:18:55 +01:00
olebeck 006f604436 fix issue with subchunk servers being labelled as empty 2023-03-11 16:50:54 +01:00
olebeck 8b763cde79 add skinid to customdata fields 2023-03-11 16:05:26 +01:00
olebeck b73ef84c1f fix action 2023-03-10 10:52:59 +01:00
olebeck f2dc5000dc fix env 2023-03-09 16:24:39 +01:00
olebeck c5e7237273 env not properly applied 2023-03-09 14:58:54 +01:00
olebeck c94d7038e9 update ci 2023-03-08 17:35:51 +01:00
olebeck 2bf6f3c534 typo. 2023-03-08 12:46:38 +01:00
olebeck 12ee9d2bcf make the worlds ui semi usable 2023-03-08 12:46:16 +01:00
olebeck c9850772d5 rework ui code to allow writing uis for subcommands 2023-03-06 21:49:30 +01:00
olebeck 5565301f11 fix accidental includes, some settings not showing 2023-03-06 17:55:36 +01:00
olebeck f643e59e3f wrong path for windows 2023-03-06 17:03:45 +01:00
olebeck 818aa73a3e add android build 2023-03-06 16:47:02 +01:00
olebeck 06cf948913 add arch selection to build script, send current version when fetching updates 2023-03-06 16:17:01 +01:00
olebeck ee156a1271 seperate gui / cli builds 2023-03-06 15:50:36 +01:00
olebeck fbb2305d1c disable it actually 2023-03-06 11:56:28 +01:00
olebeck 3e6da4ebfe disabled go vcs 2023-03-06 11:54:00 +01:00
olebeck f661895d2f install fyne 2023-03-06 02:05:33 +01:00
olebeck 5a54b98fd7 update ci to build gui 2023-03-06 02:03:31 +01:00
olebeck 5b5ddc92bb add a gui settings thing 2023-03-05 22:50:58 +01:00
olebeck e8062430e7 update dragonfly 2023-03-05 20:44:04 +01:00
olebeck c93cf7d0f9 fix full image render 2023-03-05 20:08:30 +01:00
olebeck 17572e8e94 fix wrong skins being saved, save skins immediately 2023-03-04 03:25:13 +01:00
olebeck 5485182135 1.19.63 2023-02-26 02:19:10 +01:00
olebeck 8365adb4cd add deviceID to custom data 2023-02-23 10:23:35 +01:00
olebeck d6b2f53f48 dont ignore some errors 2023-02-22 17:47:40 +01:00
olebeck e53208969e allow connection errors 2023-02-22 16:48:24 +01:00
olebeck 6acb821a5a update gophertunnel 2023-02-22 11:22:39 +01:00
olebeck 956448d707 fix build 2023-02-18 21:49:44 +01:00
olebeck f099996de7 update gophertunnel (support 1.19.62) 2023-02-18 20:39:45 +01:00
olebeck 8450304c72 update gophertunnel 2023-02-13 01:44:41 +01:00
olebeck c4cc819dc2 update gophertunnel 2023-02-13 01:32:27 +01:00
olebeck 6670d575d7 allow custom user data (untested) 2023-02-12 22:22:44 +01:00
olebeck 4d4eb37647 oops 2023-02-08 20:02:05 +01:00
olebeck 55916e4e55 rewrite skins to output skin packs 2023-02-08 19:08:26 +01:00
olebeck 2251d7096d go.sum 2023-02-08 12:00:54 +01:00
olebeck a306a6e77f fix map item 2023-02-05 17:41:06 +01:00
olebeck a601cb33a0 fix pixelparadise bug 2023-02-05 02:03:14 +01:00
olebeck aed76d6372 fix issue with cubecraft 2023-01-29 23:07:42 +01:00
olebeck 3a5a13aa75 code formatting 2023-01-29 21:20:13 +00:00
olebeck 32f1e8019a add custom items, (untested probably not working entities) 2023-01-29 15:08:41 +00:00
olebeck ea6dea7953 add custom items, use proxy code for everything 2023-01-29 01:44:32 +00:00
olebeck e84b3ea2fe fix skins command not properly closing, extra-debug now force enables debug 2023-01-28 23:59:58 +00:00
olebeck 0576f3ce63 add extremely verbose packet log 2023-01-27 13:07:53 +01:00
olebeck ec5bae8b8f fix blocknbt 2023-01-26 16:04:32 +01:00
olebeck a4affc7d23 fix issue saving custom block worlds, may have issues above 1.17 still 2023-01-26 15:30:18 +01:00
olebeck a4a293ef52 enable creator experiments in the world if custom blocks exist 2023-01-25 22:12:49 +01:00
olebeck 2d3650f001 update dragonfly 2023-01-25 15:15:48 +01:00
olebeck 9e99a9206b fix mistakes in german translation 2023-01-25 14:13:07 +01:00
olebeck 2f9c74ae5c update dragonfly, put captures in folder 2023-01-25 13:51:00 +01:00
olebeck 12417c63a3 update dragonfly 2023-01-24 22:41:49 +01:00
olebeck d5dec0af98 update dragonfly 2023-01-24 22:41:37 +01:00
olebeck 56ac765b1d fixing behavior pack textures 2023-01-24 12:50:27 +01:00
olebeck 3dba4525b5 generate proper behavior packs, chatgpt translation to german 2023-01-24 10:49:48 +01:00
olebeck 1cc242680a save bp to folder 2023-01-23 20:03:21 +01:00
olebeck 1537c7f705 hopefully this actually fixed it 2023-01-23 19:48:35 +01:00
olebeck 1160e025ec fix translations 2023-01-23 19:18:01 +01:00
olebeck 7d2974c9c4
Merge pull request #30 from itz-depression/master
added owo language
2023-01-23 17:18:08 +01:00
itz_Deprestion 75396f908f
Update owo.yaml 2023-01-23 16:13:42 +00:00
itz_Deprestion 77d47124c8
added owo language 2023-01-23 15:11:50 +00:00
olebeck f80061543c add missing translations 2023-01-23 13:50:42 +01:00
olebeck 47f85f78b3 updating dependencies, adding translation support 2023-01-23 13:40:12 +01:00
94 changed files with 8273 additions and 2442 deletions

2
.gitattributes vendored
View File

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

View File

@ -8,7 +8,8 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
A clear and concise description of what the bug is,
and what server it occurs on.
**To Reproduce**
Steps to reproduce the behavior:
@ -24,15 +25,20 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- OS: [e.g. windows]
- Version [e.g. 1.28.0-36]
- Minecraft Version [e.g. 1.19.73]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
- OS: [e.g. iOS12]
- Version [e.g. 1.28.0-36]
**Additional context**
Add any other context about the problem here.
**attach packets.log.gpg (not always necessary)**
this file can be helpful for debugging without having to connect to the server.
it can be created by running with -extra-debug [e.g. bedrocktool.exe -extra-debug worlds -address play.mojang.com ]
be sure to only attach the .gpg file which is not publicly readable.

View File

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

17
.gitignore vendored
View File

@ -8,6 +8,7 @@ token.json
*.bmp
*.bin
*.log
*.syso
/bedrocktool
/bedrocktool-*
@ -17,6 +18,18 @@ __debug_bin
keys.db
/skins/
/worlds/
/packs/
/dist/
/public/
/builds/
/builds/
/updates/
/fyne-cross/
/tmp/
/cmd/test
packets.log.gpg
customdata.json
/other-projects/
__pycache__/entityflags.cpython-310.pyc
gathering.py
*.ipynb
entityflags.py

View File

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

187
build.py Normal file
View File

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

View File

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

75
go.mod
View File

@ -1,54 +1,81 @@
module github.com/bedrock-tool/bedrocktool
go 1.19
go 1.20
//replace github.com/sandertv/gophertunnel => ./gophertunnel
replace github.com/sandertv/gophertunnel => github.com/olebeck/gophertunnel v1.26.1
replace github.com/sandertv/gophertunnel => github.com/olebeck/gophertunnel v1.29.0-1
//replace github.com/df-mc/dragonfly => ./dragonfly
replace github.com/df-mc/dragonfly => github.com/olebeck/dragonfly v0.8.10-1
replace github.com/df-mc/dragonfly => github.com/olebeck/dragonfly v0.9.4-13
//replace gioui.org => ./gio
replace gioui.org => github.com/olebeck/gio v0.0.0-20230427194143-c9c9d8bc704d
require (
github.com/df-mc/dragonfly v0.8.5
gioui.org v0.0.0-20230427133431-816bda7ac7bd
gioui.org/x v0.0.0-20230426160849-752f112c7a59
github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/df-mc/dragonfly v0.9.4
github.com/df-mc/goleveldb v1.1.9
github.com/fatih/color v1.13.0
github.com/flytam/filenamify v1.1.1
github.com/fatih/color v1.15.0
github.com/flytam/filenamify v1.1.2
github.com/go-gl/mathgl v1.0.0
github.com/google/subcommands v1.2.0
github.com/miekg/dns v1.1.50
github.com/google/uuid v1.3.0
github.com/jinzhu/copier v0.3.5
github.com/miekg/dns v1.1.53
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/repeale/fp-go v0.11.1
github.com/sanbornm/go-selfupdate v0.0.0-20210106163404-c9b625feac49
github.com/sandertv/gophertunnel v1.26.0
github.com/sandertv/go-raknet v1.12.0
github.com/sandertv/gophertunnel v1.29.0
github.com/shirou/gopsutil/v3 v3.23.3
github.com/sirupsen/logrus v1.9.0
golang.design/x/lockfree v0.0.1
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
golang.org/x/crypto v0.8.0
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
golang.org/x/oauth2 v0.7.0
golang.org/x/text v0.9.0
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v3 v3.0.1
)
require (
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
gioui.org/shader v1.0.6 // indirect
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/changkun/lockfree v0.0.1 // indirect
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 // indirect
github.com/df-mc/atomic v1.10.0 // indirect
github.com/df-mc/worldupgrader v1.0.3 // indirect
github.com/dlclark/regexp2 v1.9.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0
github.com/jinzhu/copier v0.3.5
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/klauspost/compress v1.15.11 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/kr/binarydist v0.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/muhammadmuzzammil1998/jsonc v1.0.0 // indirect
github.com/sandertv/go-raknet v1.12.0 // indirect
github.com/sirupsen/logrus v1.9.0
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/shoenig/go-m1cpu v0.1.4 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 // indirect
golang.org/x/image v0.7.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inconshreveable/go-update.v0 v0.0.0-20150814200126-d8b0b1d421aa // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
)

189
go.sum
View File

@ -1,7 +1,27 @@
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/x v0.0.0-20230426160849-752f112c7a59 h1:IdvHx1hSmuL9xs/p3rppWJnH01RXcBhz5HvB7pj9LAg=
gioui.org/x v0.0.0-20230426160849-752f112c7a59/go.mod h1:nMctdnZS2HKxfSXb+bCPnhw1n2LLsXoxtTarZjtIBuI=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 h1:/G0ghZwrhou0Wq21qc1vXXMm/t/aKWkALWwITptKbE0=
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/changkun/lockfree v0.0.1 h1:5WefVJLglY4IHRqOQmh6Ao6wkJYaJkarshKU8VUtId4=
github.com/changkun/lockfree v0.0.1/go.mod h1:3bKiaXn/iNzIPlSvSOMSVbRQUQtAp8qUAyBUtzU11s4=
github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:Yg2hDs4b13Evkpj42FU2idX2cVXVFqQSheXYKM86Qsk=
github.com/cloudfoundry-attic/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:MgJyK38wkzZbiZSKeIeFankxxSA8gayko/nr5x5bgBA=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -9,13 +29,22 @@ github.com/df-mc/atomic v1.10.0 h1:0ZuxBKwR/hxcFGorKiHIp+hY7hgY+XBTzhCYD2NqSEg=
github.com/df-mc/atomic v1.10.0/go.mod h1:Gw9rf+rPIbydMjA329Jn4yjd/O2c/qusw3iNp4tFGSc=
github.com/df-mc/goleveldb v1.1.9 h1:ihdosZyy5jkQKrxucTQmN90jq/2lUwQnJZjIYIC/9YU=
github.com/df-mc/goleveldb v1.1.9/go.mod h1:+NHCup03Sci5q84APIA21z3iPZCuk6m6ABtg4nANCSk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/flytam/filenamify v1.1.1 h1:508gP8QR6vtbv46S3oz2ob9l7JGFdDFfqqMeh/TwzTk=
github.com/flytam/filenamify v1.1.1/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
github.com/df-mc/worldupgrader v1.0.3 h1:3nbthy6vfSNQZdqHBR+E5Fh3mCeWmCwLtqrYDiPUG5I=
github.com/df-mc/worldupgrader v1.0.3/go.mod h1:6ybkJ/BV9b0XkcPzcLmvgT9Nv/xgBXdDQTmRhu7b8zQ=
github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI=
github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/flytam/filenamify v1.1.2 h1:dGlfWU4zrhDlsmvob4IFcfgjG5vIjfo4UwLyec6Wx94=
github.com/flytam/filenamify v1.1.2/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68=
github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae h1:LCcaQgYrnS+sx9Tc3oGUvbRBRt+5oFnKWakaxeAvNVI=
github.com/go-text/typesetting v0.0.0-20230413204129-b4f0492bf7ae/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA=
github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc h1:9Kf84pnrmmjdRzZIkomfjowmGUhHs20jkrWYw/I6CYc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@ -25,7 +54,9 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@ -37,35 +68,29 @@ github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo=
github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/muhammadmuzzammil1998/jsonc v1.0.0 h1:8o5gBQn4ZA3NBA9DlTujCj2a4w0tqWrPVjDwhzkgTIs=
github.com/muhammadmuzzammil1998/jsonc v1.0.0/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/olebeck/dragonfly v0.8.5-1 h1:+z1WO0W4jCc1jcPMGDz85AxLWz59haEakD0yAVEXeWQ=
github.com/olebeck/dragonfly v0.8.5-1/go.mod h1:CHQ21j4LjSwHgUeMk06tcxEW6U+SI7avLCUeSvp9AXo=
github.com/olebeck/dragonfly v0.8.10-1 h1:oMeCcyoKfb9xEDxxbKM6YyUPlK/R8wix/zzZ4UzkYYQ=
github.com/olebeck/dragonfly v0.8.10-1/go.mod h1:ZjzPME6I1nc73voUgr2s5lpkoTxnWuR54V6c1KbULX0=
github.com/olebeck/gophertunnel v1.24.11-1 h1:DKEqjK5nAcjlwejHAD/SHeolAM6y9EkyjJcMuoK7VVY=
github.com/olebeck/gophertunnel v1.24.11-1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/olebeck/gophertunnel v1.24.13-1 h1:3Sj22hVFAGCnPMJAewoSeuC9N8ikzcWo0DvJNBnXvs0=
github.com/olebeck/gophertunnel v1.24.13-1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/olebeck/gophertunnel v1.25.0-1 h1:9vh6MIUo7/3EJI5oULr5F9KutAoa9qnpgWa566fImaI=
github.com/olebeck/gophertunnel v1.25.0-1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/olebeck/gophertunnel v1.26.1 h1:mkIthqpU5vSksh/mxUlhqFN/nsK9CcP9hAn1LjjqKnQ=
github.com/olebeck/gophertunnel v1.26.1/go.mod h1:dYFetA6r62huhc1EgR9p8VFAFtKOuGgVE/iXf5CzZ4o=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/olebeck/dragonfly v0.9.4-13 h1:JF72hfG3/BBCXU1GSBaEKKXQy/gt+0mEOua3RdKXdJ8=
github.com/olebeck/dragonfly v0.9.4-13/go.mod h1:ZNcbAATEeTNyN3Cumtwzox7STtFve469HHzL5c1K3nY=
github.com/olebeck/gio v0.0.0-20230427194143-c9c9d8bc704d h1:D+Ryca52xv37/p0FsEWfGwAGUZ1vPWpvimA2eMfBijc=
github.com/olebeck/gio v0.0.0-20230427194143-c9c9d8bc704d/go.mod h1:8CFQM/4LurRd9G3NUYdacFb9j2pK0LrAyVO2mAZo4mw=
github.com/olebeck/gophertunnel v1.29.0-1 h1:3x2cZoe8O54xVFgEZqTBJpFEXlzbjlLFoo/d9cWGv+g=
github.com/olebeck/gophertunnel v1.29.0-1/go.mod h1:HxQfl/8mZzvjzhekEH8RO6xLAgan9i/wIyrQzw0tIPY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -73,110 +98,114 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/repeale/fp-go v0.11.1 h1:Q/e+gNyyHaxKAyfdbBqvip3DxhVWH453R+kthvSr9Mk=
github.com/repeale/fp-go v0.11.1/go.mod h1:4KrwQJB1VRY+06CA+jTc4baZetr6o2PeuqnKr5ybQUc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sanbornm/go-selfupdate v0.0.0-20210106163404-c9b625feac49 h1:LuxslTBxJrrNeKfqoywIERWWhH43TgiVAiPEVlhgNBA=
github.com/sanbornm/go-selfupdate v0.0.0-20210106163404-c9b625feac49/go.mod h1:fY313ZGG810aWruFYcyq3coFpHDrWJVoMfSRI81y1r4=
github.com/sandertv/go-raknet v1.12.0 h1:olUzZlIJyX/pgj/mrsLCZYjKLNDsYiWdvQ4NIm3z0DA=
github.com/sandertv/go-raknet v1.12.0/go.mod h1:Gx+WgZBMQ0V2UoouGoJ8Wj6CDrMBQ4SB2F/ggpl5/+Y=
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
github.com/shoenig/go-m1cpu v0.1.4 h1:SZPIgRM2sEF9NJy50mRHu9PKGwxyyTTJIWvCtgVbozs=
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.design/x/lockfree v0.0.1 h1:IHFNwZgM5bnZYWkEbzn5lWHMYr8WsRBdCJ/RBVY0xMM=
golang.design/x/lockfree v0.0.1/go.mod h1:iaZUx6UgZaOdePjzI6wFd+seYMl1i0rsG8+xKvA8c4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b h1:uKO3Js8lXGjpjdc4J3rqs0/Ex5yDKUGfk43tTYWVLas=
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@ -188,5 +217,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

71
handlers/capture.go Normal file
View File

@ -0,0 +1,71 @@
package handlers
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"os"
"sync"
"time"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)
func (p *packetCapturer) dumpPacket(toServer bool, payload []byte) {
p.dumpLock.Lock()
defer p.dumpLock.Unlock()
p.fio.Write([]byte{0xAA, 0xAA, 0xAA, 0xAA})
packetSize := uint32(len(payload))
binary.Write(p.fio, binary.LittleEndian, packetSize)
binary.Write(p.fio, binary.LittleEndian, toServer)
binary.Write(p.fio, binary.LittleEndian, time.Now().UnixMilli())
p.fio.Write(payload)
p.fio.Write([]byte{0xBB, 0xBB, 0xBB, 0xBB})
}
type packetCapturer struct {
proxy *utils.ProxyContext
fio *os.File
dumpLock sync.Mutex
}
func (p *packetCapturer) AddressAndName(address, hostname string) error {
os.Mkdir("captures", 0o775)
fio, err := os.Create(fmt.Sprintf("captures/%s-%s.pcap2", hostname, time.Now().Format("2006-01-02_15-04-05")))
if err != nil {
return err
}
utils.WriteReplayHeader(fio)
p.fio = fio
return nil
}
func (p *packetCapturer) PacketFunc(header packet.Header, payload []byte, src, dst net.Addr) {
buf := bytes.NewBuffer(nil)
header.Write(buf)
buf.Write(payload)
p.dumpPacket(p.proxy.IsClient(src), buf.Bytes())
}
func NewPacketCapturer() *utils.ProxyHandler {
p := &packetCapturer{}
return &utils.ProxyHandler{
Name: "Packet Capturer",
ProxyRef: func(pc *utils.ProxyContext) {
p.proxy = pc
},
AddressAndName: p.AddressAndName,
PacketFunc: p.PacketFunc,
OnEnd: func() {
p.dumpLock.Lock()
defer p.dumpLock.Unlock()
p.fio.Close()
},
}
}
func init() {
utils.NewPacketCapturer = NewPacketCapturer
}

52
handlers/chat_log.go Normal file
View File

@ -0,0 +1,52 @@
package handlers
import (
"fmt"
"os"
"time"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type chatLogger struct {
Verbose bool
fio *os.File
}
func (c *chatLogger) PacketCB(pk packet.Packet, toServer bool, t time.Time) (packet.Packet, error) {
if text, ok := pk.(*packet.Text); ok {
logLine := text.Message
if c.Verbose {
logLine += fmt.Sprintf(" (TextType: %d | XUID: %s | PlatformChatID: %s)", text.TextType, text.XUID, text.PlatformChatID)
}
c.fio.WriteString(fmt.Sprintf("[%s] ", t.Format(time.RFC3339)))
logrus.Info(logLine)
if toServer {
c.fio.WriteString("SENT: ")
}
c.fio.WriteString(logLine + "\n")
}
return pk, nil
}
func NewChatLogger() *utils.ProxyHandler {
c := &chatLogger{}
return &utils.ProxyHandler{
Name: "Packet Capturer",
PacketCB: c.PacketCB,
AddressAndName: func(address, hostname string) error {
filename := fmt.Sprintf("%s_%s_chat.log", hostname, time.Now().Format("2006-01-02_15-04-05_Z07"))
f, err := os.Create(filename)
if err != nil {
return err
}
c.fio = f
return nil
},
OnEnd: func() {
c.fio.Close()
},
}
}

122
handlers/packet_logger.go Normal file
View File

@ -0,0 +1,122 @@
package handlers
import (
"bufio"
"io"
"net"
"os"
"reflect"
"sync"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/bedrock-tool/bedrocktool/utils/crypt"
"github.com/fatih/color"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
var MutedPackets = []string{
"packet.UpdateBlock",
"packet.MoveActorAbsolute",
"packet.SetActorMotion",
"packet.SetTime",
"packet.RemoveActor",
"packet.AddActor",
"packet.UpdateAttributes",
"packet.Interact",
"packet.LevelEvent",
"packet.SetActorData",
"packet.MoveActorDelta",
"packet.MovePlayer",
"packet.BlockActorData",
"packet.PlayerAuthInput",
"packet.LevelChunk",
"packet.LevelSoundEvent",
"packet.ActorEvent",
"packet.NetworkChunkPublisherUpdate",
"packet.UpdateSubChunkBlocks",
"packet.SubChunk",
"packet.SubChunkRequest",
"packet.Animate",
"packet.NetworkStackLatency",
"packet.InventoryTransaction",
"packet.PlaySound",
}
var dirS2C = color.GreenString("S") + "->" + color.CyanString("C")
var dirC2S = color.CyanString("C") + "->" + color.GreenString("S")
var pool = packet.NewPool()
func NewDebugLogger(extraVerbose bool) *utils.ProxyHandler {
var logPlain, logCrypt, logCryptEnc io.WriteCloser
var packetsLogF *bufio.Writer
var dmpLock sync.Mutex
if extraVerbose {
// open plain text log
logPlain, err := os.Create("packets.log")
if err != nil {
logrus.Error(err)
}
// open gpg log
logCryptEnc, err = crypt.Encer("packets.log.gpg")
if err != nil {
logrus.Error(err)
}
if logPlain != nil || logCryptEnc != nil {
packetsLogF = bufio.NewWriter(io.MultiWriter(logPlain, logCryptEnc))
}
}
var proxy *utils.ProxyContext
return &utils.ProxyHandler{
Name: "Debug",
ProxyRef: func(pc *utils.ProxyContext) {
proxy = pc
},
PacketFunc: func(header packet.Header, payload []byte, src, dst net.Addr) {
pk := utils.DecodePacket(header, payload)
if pk == nil {
return
}
if packetsLogF != nil {
dmpLock.Lock()
packetsLogF.Write([]byte(utils.DumpStruct(0, pk, true, false) + "\n\n\n"))
dmpLock.Unlock()
}
pkName := reflect.TypeOf(pk).String()[1:]
if !slices.Contains(MutedPackets, pkName) {
var dir string = dirS2C
if proxy.IsClient(src) {
dir = dirC2S
}
logrus.Debugf("%s 0x%02x, %s", dir, pk.ID(), pkName)
}
},
OnEnd: func() {
dmpLock.Lock()
if packetsLogF != nil {
packetsLogF.Flush()
}
if logPlain != nil {
logPlain.Close()
}
if logCryptEnc != nil {
logCryptEnc.Close()
}
if logCrypt != nil {
logCrypt.Close()
}
dmpLock.Unlock()
},
}
}
func init() {
// hacky solution to allow proxy to add this
utils.NewDebugLogger = NewDebugLogger
}

View File

@ -0,0 +1,123 @@
package seconduser
import (
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
func (s *secondaryUser) ResetWorld() {
s.chunks = make(map[world.ChunkPos]*chunk.Chunk)
s.blockNBTs = make(map[protocol.BlockPos][]map[string]any)
}
func (s *secondaryUser) processChangeDimension(pk *packet.ChangeDimension) {
s.ResetWorld()
dimensionID := pk.Dimension
if s.ispre118 && dimensionID == 0 {
dimensionID += 10
}
s.dimension, _ = world.DimensionByID(int(dimensionID))
}
func (s *secondaryUser) processLevelChunk(pk *packet.LevelChunk) {
// ignore empty chunks THANKS WEIRD SERVER SOFTWARE DEVS
if len(pk.RawPayload) == 0 {
return
}
var subChunkCount int
switch pk.SubChunkCount {
case protocol.SubChunkRequestModeLimited:
fallthrough
case protocol.SubChunkRequestModeLimitless:
subChunkCount = 0
default:
subChunkCount = int(pk.SubChunkCount)
}
ch, blockNBTs, err := chunk.NetworkDecode(world.AirRID(), pk.RawPayload, subChunkCount, s.dimension.Range(), s.ispre118, s.hasCustomBlocks)
if err != nil {
logrus.Error(err)
return
}
for _, blockNBT := range blockNBTs {
x := blockNBT["x"].(int32)
y := blockNBT["y"].(int32)
z := blockNBT["z"].(int32)
s.blockNBTs[protocol.BlockPos{x, y, z}] = blockNBTs
}
s.chunks[world.ChunkPos(pk.Position)] = ch
for _, p := range s.server.Players() {
p.Session().ViewChunk(world.ChunkPos(pk.Position), ch, nil)
}
max := s.dimension.Range().Height() / 16
switch pk.SubChunkCount {
case protocol.SubChunkRequestModeLimited:
max = int(pk.HighestSubChunk)
fallthrough
case protocol.SubChunkRequestModeLimitless:
var offsetTable []protocol.SubChunkOffset
r := s.dimension.Range()
for y := int8(r.Min() / 16); y < int8(r.Max()); y++ {
offsetTable = append(offsetTable, protocol.SubChunkOffset{0, y, 0})
}
dimId, _ := world.DimensionID(s.dimension)
s.proxy.Server.WritePacket(&packet.SubChunkRequest{
Dimension: int32(dimId),
Position: protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
},
Offsets: offsetTable[:max],
})
}
}
func (s *secondaryUser) processSubChunk(pk *packet.SubChunk) {
offsets := make(map[world.ChunkPos]bool, len(pk.SubChunkEntries))
for _, sub := range pk.SubChunkEntries {
var (
absX = pk.Position[0] + int32(sub.Offset[0])
absY = pk.Position[1] + int32(sub.Offset[1])
absZ = pk.Position[2] + int32(sub.Offset[2])
pos = world.ChunkPos{absX, absZ}
)
offsets[pos] = true
ch, ok := s.chunks[pos]
if !ok {
logrus.Error(locale.Loc("subchunk_before_chunk", nil))
continue
}
blockNBTs, err := ch.ApplySubChunkEntry(uint8(absY), &sub)
if err != nil {
logrus.Error(err)
}
for _, blockNBT := range blockNBTs {
x := blockNBT["x"].(int32)
y := blockNBT["y"].(int32)
z := blockNBT["z"].(int32)
s.blockNBTs[protocol.BlockPos{x, y, z}] = blockNBTs
}
chunk.LightArea([]*chunk.Chunk{ch}, 0, 0).Fill()
}
for _, p := range s.server.Players() {
for pos := range offsets {
ch, ok := s.chunks[pos]
if !ok {
continue
}
p.Session().ViewChunk(pos, ch, nil)
}
}
}

View File

@ -0,0 +1,48 @@
package seconduser
import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
)
type entityType struct {
name string
}
func (t *entityType) EncodeEntity() string {
return t.name
}
func (t *entityType) BBox(e world.Entity) cube.BBox {
return cube.BBox{}
}
type serverEntity struct {
t world.EntityType
pos mgl64.Vec3
rot cube.Rotation
}
func (s *serverEntity) Close() error {
return nil
}
func (s *serverEntity) Position() mgl64.Vec3 {
return s.pos
}
func (s *serverEntity) Rotation() cube.Rotation {
return s.rot
}
func (e *serverEntity) World() *world.World {
w, _ := world.OfEntity(e)
return w
}
func (s *serverEntity) Type() world.EntityType {
return s.t
}
func newServerEntity(typename string) *serverEntity {
return &serverEntity{
t: &entityType{name: typename},
}
}

View File

@ -0,0 +1,19 @@
package seconduser
import "github.com/df-mc/dragonfly/server/session"
type fwdlistener struct {
Conn chan session.Conn
}
func (l *fwdlistener) Accept() (session.Conn, error) {
return <-l.Conn, nil
}
func (l *fwdlistener) Disconnect(conn session.Conn, reason string) error {
return conn.Close()
}
func (l *fwdlistener) Close() error {
return nil
}

View File

@ -0,0 +1,48 @@
package seconduser
import (
"sync"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/google/uuid"
)
type provider struct {
s *secondaryUser
}
func (p *provider) Settings() *world.Settings {
return &world.Settings{
Mutex: sync.Mutex{},
Name: "world",
Spawn: cube.Pos{0, 0, 0},
DefaultGameMode: world.GameModeCreative,
Difficulty: world.DifficultyNormal,
}
}
func (p *provider) SaveSettings(*world.Settings) {
}
func (p *provider) Close() error {
return nil
}
func (p *provider) LoadPlayerSpawnPosition(uuid uuid.UUID) (pos cube.Pos, exists bool, err error) {
return cube.Pos{0, 0, 0}, false, nil
}
func (p *provider) SavePlayerSpawnPosition(uuid uuid.UUID, pos cube.Pos) error {
return nil
}
func (p *provider) LoadColumn(pos world.ChunkPos, dim world.Dimension) (*world.Column, error) {
return &world.Column{
Chunk: p.s.chunks[pos],
}, nil
}
func (p *provider) StoreColumn(pos world.ChunkPos, dim world.Dimension, col *world.Column) error {
return nil
}

View File

@ -0,0 +1,113 @@
package seconduser
import (
"time"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/df-mc/dragonfly/server"
"github.com/df-mc/dragonfly/server/player"
"github.com/df-mc/dragonfly/server/player/skin"
"github.com/df-mc/dragonfly/server/session"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/go-gl/mathgl/mgl64"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type secondaryUser struct {
listener *fwdlistener
server *server.Server
proxy *utils.ProxyContext
ispre118 bool
hasCustomBlocks bool
chunks map[world.ChunkPos]*chunk.Chunk
blockNBTs map[protocol.BlockPos][]map[string]any
dimension world.Dimension
entities map[int64]*serverEntity
mainPlayer *player.Player
}
func NewSecondUser() *utils.ProxyHandler {
s := &secondaryUser{
listener: &fwdlistener{
Conn: make(chan session.Conn),
},
chunks: make(map[world.ChunkPos]*chunk.Chunk),
blockNBTs: make(map[protocol.BlockPos][]map[string]any),
dimension: world.Overworld,
entities: make(map[int64]*serverEntity),
}
s.server = server.Config{
Listeners: []func(conf server.Config) (server.Listener, error){
func(conf server.Config) (server.Listener, error) {
return s.listener, nil
},
},
Log: logrus.StandardLogger(),
Name: "Secondary",
Generator: func(dim world.Dimension) world.Generator { return &world.NopGenerator{} },
WorldProvider: &provider{s: s},
ReadOnlyWorld: true,
}.New()
go s.loop()
return &utils.ProxyHandler{
Name: "Secondary User",
ProxyRef: func(pc *utils.ProxyContext) {
s.proxy = pc
},
SecondaryClientCB: s.SecondaryClientCB,
OnClientConnect: func(conn minecraft.IConn) {
id := conn.IdentityData()
s.mainPlayer = player.New(id.DisplayName, skin.New(64, 64), mgl64.Vec3{0, 00})
s.server.World().AddEntity(s.mainPlayer)
},
PacketCB: func(pk packet.Packet, toServer bool, timeReceived time.Time) (packet.Packet, error) {
switch pk := pk.(type) {
case *packet.LevelChunk:
s.processLevelChunk(pk)
case *packet.SubChunk:
s.processSubChunk(pk)
case *packet.ChangeDimension:
s.processChangeDimension(pk)
case *packet.MovePlayer:
v := mgl64.Vec3{float64(pk.Position.X()), float64(pk.Position.Y()), float64(pk.Position.Z())}
s.mainPlayer.Teleport(v)
case *packet.PlayerAuthInput:
v := mgl64.Vec3{float64(pk.Position.X()), float64(pk.Position.Y()), float64(pk.Position.Z())}
s.mainPlayer.Teleport(v)
case *packet.AddActor:
e := newServerEntity(pk.EntityType)
s.entities[pk.EntityUniqueID] = e
s.server.World().AddEntity(e)
}
return pk, nil
},
}
}
func (s *secondaryUser) SecondaryClientCB(conn minecraft.IConn) {
s.listener.Conn <- conn
}
func (s *secondaryUser) loop() {
s.server.Listen()
for s.server.Accept(func(p *player.Player) {
logrus.Infof("%s Joined", p.Name())
p.Teleport(s.mainPlayer.Position())
}) {
}
}

127
handlers/skins.go Normal file
View File

@ -0,0 +1,127 @@
package handlers
import (
"fmt"
"os"
"path"
"strings"
"time"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type SkinSaver struct {
PlayerNameFilter string
OnlyIfHasGeometry bool
ServerName string
Proxy *utils.ProxyContext
fpath string
playerSkinPacks map[uuid.UUID]*utils.SkinPack
playerNames map[uuid.UUID]string
}
func (s *SkinSaver) AddPlayerSkin(playerID uuid.UUID, playerName string, skin *utils.Skin) (added bool) {
p, ok := s.playerSkinPacks[playerID]
if !ok {
creating := fmt.Sprintf("Creating Skinpack for %s", playerName)
s.Proxy.SendPopup(creating)
logrus.Info(creating)
p = utils.NewSkinPack(playerName, s.fpath)
s.playerSkinPacks[playerID] = p
}
if p.AddSkin(skin) {
if ok {
addedStr := fmt.Sprintf("Added a skin to %s", playerName)
s.Proxy.SendPopup(addedStr)
logrus.Info(addedStr)
}
added = true
}
if err := p.Save(path.Join(s.fpath, playerName), s.ServerName); err != nil {
logrus.Error(err)
}
return added
}
func (s *SkinSaver) AddSkin(playerName string, playerID uuid.UUID, playerSkin *protocol.Skin) (string, *utils.Skin, bool) {
if playerName == "" {
playerName = s.playerNames[playerID]
if playerName == "" {
playerName = playerID.String()
}
}
if !strings.HasPrefix(playerName, s.PlayerNameFilter) {
return playerName, nil, false
}
s.playerNames[playerID] = playerName
skin := &utils.Skin{Skin: playerSkin}
if s.OnlyIfHasGeometry && !skin.HaveGeometry() {
return playerName, nil, false
}
wasAdded := s.AddPlayerSkin(playerID, playerName, skin)
return playerName, skin, wasAdded
}
type SkinAdd struct {
PlayerName string
Skin *protocol.Skin
}
func (s *SkinSaver) ProcessPacket(pk packet.Packet) (out []SkinAdd) {
switch pk := pk.(type) {
case *packet.PlayerList:
if pk.ActionType == 1 { // remove
return nil
}
for _, player := range pk.Entries {
playerName, skin, wasAdded := s.AddSkin(utils.CleanupName(player.Username), player.UUID, &player.Skin)
if wasAdded {
out = append(out, SkinAdd{
PlayerName: playerName,
Skin: skin.Skin,
})
}
}
case *packet.AddPlayer:
if _, ok := s.playerNames[pk.UUID]; !ok {
s.playerNames[pk.UUID] = utils.CleanupName(pk.Username)
}
}
return out
}
func NewSkinSaver(skinCB func(SkinAdd)) *utils.ProxyHandler {
s := &SkinSaver{
playerSkinPacks: make(map[uuid.UUID]*utils.SkinPack),
playerNames: make(map[uuid.UUID]string),
}
return &utils.ProxyHandler{
Name: "Skin Saver",
ProxyRef: func(pc *utils.ProxyContext) {
s.Proxy = pc
},
AddressAndName: func(address, hostname string) error {
outPathBase := fmt.Sprintf("skins/%s", hostname)
os.MkdirAll(outPathBase, 0o755)
s.fpath = outPathBase
return nil
},
PacketCB: func(pk packet.Packet, toServer bool, timeReceived time.Time) (packet.Packet, error) {
if !toServer {
for _, s := range s.ProcessPacket(pk) {
if skinCB != nil {
skinCB(s)
}
}
}
return pk, nil
},
}
}

168
handlers/worlds/chunk.go Normal file
View File

@ -0,0 +1,168 @@
package worlds
import (
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/repeale/fp-go"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
func (w *worldsHandler) processChangeDimension(pk *packet.ChangeDimension) {
w.SaveAndReset()
dimensionID := pk.Dimension
if w.serverState.ispre118 && dimensionID == 0 {
dimensionID += 10
}
w.worldState.dimension, _ = world.DimensionByID(int(dimensionID))
}
func (w *worldsHandler) processLevelChunk(pk *packet.LevelChunk) {
// ignore empty chunks THANKS WEIRD SERVER SOFTWARE DEVS
if len(pk.RawPayload) == 0 {
logrus.Info(locale.Loc("empty_chunk", nil))
return
}
var subChunkCount int
switch pk.SubChunkCount {
case protocol.SubChunkRequestModeLimited:
fallthrough
case protocol.SubChunkRequestModeLimitless:
subChunkCount = 0
default:
subChunkCount = int(pk.SubChunkCount)
}
ch, blockNBTs, err := chunk.NetworkDecode(world.AirRID(), pk.RawPayload, subChunkCount, w.worldState.dimension.Range(), w.serverState.ispre118, w.bp.HasBlocks())
if err != nil {
logrus.Error(err)
return
}
for _, blockNBT := range blockNBTs {
x := int(blockNBT["x"].(int32))
y := int(blockNBT["y"].(int32))
z := int(blockNBT["z"].(int32))
w.worldState.blockNBTs[cube.Pos{x, y, z}] = blockNBT
}
w.worldState.chunks[(world.ChunkPos)(pk.Position)] = ch
max := w.worldState.dimension.Range().Height() / 16
switch pk.SubChunkCount {
case protocol.SubChunkRequestModeLimited:
max = int(pk.HighestSubChunk)
fallthrough
case protocol.SubChunkRequestModeLimitless:
var offsetTable []protocol.SubChunkOffset
r := w.worldState.dimension.Range()
for y := int8(r.Min() / 16); y < int8(r.Max()); y++ {
offsetTable = append(offsetTable, protocol.SubChunkOffset{0, y, 0})
}
dimId, _ := world.DimensionID(w.worldState.dimension)
w.proxy.Server.WritePacket(&packet.SubChunkRequest{
Dimension: int32(dimId),
Position: protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
},
Offsets: offsetTable[:max],
})
default:
// legacy
empty := fp.Every(func(sub *chunk.SubChunk) bool {
return sub.Empty()
})(ch.Sub())
if !empty {
w.mapUI.SetChunk((world.ChunkPos)(pk.Position), ch, true)
}
}
}
func (w *worldsHandler) processSubChunk(pk *packet.SubChunk) {
posToRedraw := make(map[world.ChunkPos]bool)
for _, sub := range pk.SubChunkEntries {
var (
absX = pk.Position[0] + int32(sub.Offset[0])
absY = pk.Position[1] + int32(sub.Offset[1])
absZ = pk.Position[2] + int32(sub.Offset[2])
pos = world.ChunkPos{absX, absZ}
)
ch, ok := w.worldState.chunks[pos]
if !ok {
logrus.Error(locale.Loc("subchunk_before_chunk", nil))
continue
}
blockNBTs, err := ch.ApplySubChunkEntry(uint8(absY), &sub)
if err != nil {
logrus.Error(err)
}
for _, blockNBT := range blockNBTs {
x := int(blockNBT["x"].(int32))
y := int(blockNBT["y"].(int32))
z := int(blockNBT["z"].(int32))
w.worldState.blockNBTs[cube.Pos{x, y, z}] = blockNBT
}
posToRedraw[pos] = true
}
// redraw the chunks
for pos := range posToRedraw {
w.mapUI.SetChunk(pos, w.worldState.chunks[pos], true)
}
w.mapUI.SchedRedraw()
}
func blockPosInChunk(pos protocol.BlockPos) (uint8, int16, uint8) {
return uint8(pos.X() & 0x0f), int16(pos.Y() & 0x0f), uint8(pos.Z() & 0x0f)
}
func (w *worldsHandler) ProcessChunkPackets(pk packet.Packet) packet.Packet {
switch pk := pk.(type) {
case *packet.ChangeDimension:
w.processChangeDimension(pk)
case *packet.LevelChunk:
w.processLevelChunk(pk)
w.proxy.SendPopup(locale.Locm("popup_chunk_count", locale.Strmap{
"Count": len(w.worldState.chunks),
"Name": w.worldState.Name,
}, len(w.worldState.chunks)))
case *packet.SubChunk:
w.processSubChunk(pk)
case *packet.BlockActorData:
p := pk.Position
w.worldState.blockNBTs[cube.Pos{int(p.X()), int(p.Y()), int(p.Z())}] = pk.NBTData
case *packet.UpdateBlock:
if w.settings.BlockUpdates {
cp := world.ChunkPos{pk.Position.X() >> 4, pk.Position.Z() >> 4}
c, ok := w.worldState.chunks[cp]
if ok {
x, y, z := blockPosInChunk(pk.Position)
c.SetBlock(x, y, z, uint8(pk.Layer), pk.NewBlockRuntimeID)
w.mapUI.SetChunk(cp, c, true)
}
}
case *packet.UpdateSubChunkBlocks:
if w.settings.BlockUpdates {
cp := world.ChunkPos{pk.Position.X(), pk.Position.Z()}
c, ok := w.worldState.chunks[cp]
if ok {
for _, bce := range pk.Blocks {
x, y, z := blockPosInChunk(bce.BlockPos)
if bce.SyncedUpdateType == packet.BlockToEntityTransition {
c.SetBlock(x, y, z, 0, world.AirRID())
} else {
c.SetBlock(x, y, z, 0, bce.BlockRuntimeID)
}
}
w.mapUI.SetChunk(cp, c, true)
}
}
}
return pk
}

BIN
handlers/worlds/chunk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

View File

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

297
handlers/worlds/entity.go Normal file
View File

@ -0,0 +1,297 @@
package worlds
import (
"github.com/bedrock-tool/bedrocktool/utils/behaviourpack"
"github.com/bedrock-tool/bedrocktool/utils/nbtconv"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl32"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)
type entityState struct {
RuntimeID uint64
UniqueID int64
EntityType string
Position mgl32.Vec3
Pitch, Yaw float32
HeadYaw, BodyYaw float32
Velocity mgl32.Vec3
Metadata protocol.EntityMetadata
Inventory map[byte]map[byte]protocol.ItemInstance
Helmet *protocol.ItemInstance
Chestplate *protocol.ItemInstance
Leggings *protocol.ItemInstance
Boots *protocol.ItemInstance
}
type serverEntityType struct {
Encoded string
NBT map[string]any
}
func (t serverEntityType) EncodeEntity() string {
return t.Encoded
}
func (t serverEntityType) BBox(e world.Entity) cube.BBox {
return cube.Box(-0.5, 0, -0.5, 0.5, 1, 0.5)
}
func (t serverEntityType) DecodeNBT(m map[string]any) world.Entity {
return nil // not implemented, and never should
}
func (t serverEntityType) EncodeNBT(e world.Entity) map[string]any {
return t.NBT
}
type serverEntity struct {
world.Entity
EntityType serverEntityType
}
var _ world.SaveableEntityType = &serverEntityType{}
func (e serverEntity) Type() world.EntityType {
return e.EntityType
}
func (w *worldsHandler) processAddActor(pk *packet.AddActor) {
e, ok := w.getEntity(pk.EntityRuntimeID)
if !ok {
e = &entityState{
RuntimeID: pk.EntityRuntimeID,
UniqueID: pk.EntityUniqueID,
EntityType: pk.EntityType,
Inventory: make(map[byte]map[byte]protocol.ItemInstance),
Metadata: make(map[uint32]any),
}
w.worldState.entities[pk.EntityRuntimeID] = e
w.bp.AddEntity(behaviourpack.EntityIn{
Identifier: pk.EntityType,
Attr: pk.Attributes,
Meta: pk.EntityMetadata,
})
}
e.Position = pk.Position
e.Pitch = pk.Pitch
e.Yaw = pk.Yaw
e.BodyYaw = pk.BodyYaw
e.HeadYaw = pk.HeadYaw
e.Velocity = pk.Velocity
for k, v := range pk.EntityMetadata {
e.Metadata[k] = v
}
}
var flagNames = map[uint8]string{
protocol.EntityDataFlagSheared: "Sheared",
protocol.EntityDataFlagCaptain: "IsIllagerCaptain",
protocol.EntityDataFlagSitting: "Sitting",
protocol.EntityDataFlagBaby: "IsBaby",
protocol.EntityDataFlagTamed: "IsTamed",
protocol.EntityDataFlagTrusting: "IsTrusting",
protocol.EntityDataFlagOrphaned: "IsOrphaned",
protocol.EntityDataFlagAngry: "IsAngry",
protocol.EntityDataFlagOutOfControl: "IsOutOfControl",
protocol.EntityDataFlagSaddled: "Saddled",
protocol.EntityDataFlagChested: "Chested",
protocol.EntityDataFlagShowBottom: "ShowBottom",
protocol.EntityDataFlagGliding: "IsGliding",
protocol.EntityDataFlagSwimming: "IsSwimming",
protocol.EntityDataFlagEating: "IsEating",
protocol.EntityDataFlagScared: "IsScared",
protocol.EntityDataFlagStunned: "IsStunned",
protocol.EntityDataFlagRoaring: "IsRoaring",
}
func entityMetadataToNBT(metadata protocol.EntityMetadata, nbt map[string]any) {
if variant, ok := metadata[protocol.EntityDataKeyVariant]; ok {
nbt["Variant"] = variant
}
if markVariant, ok := metadata[protocol.EntityDataKeyMarkVariant]; ok {
nbt["MarkVariant"] = markVariant
}
if color, ok := metadata[protocol.EntityDataKeyColorIndex]; ok {
nbt["Color"] = color
}
if color2, ok := metadata[protocol.EntityDataKeyColorTwoIndex]; ok {
nbt["Color2"] = color2
}
if skinID, ok := metadata[protocol.EntityDataKeySkinID]; ok {
nbt["SkinID"] = int32(skinID.(int32))
}
if name, ok := metadata[protocol.EntityDataKeyName]; ok {
nbt["CustomName"] = name
}
if ShowNameTag, ok := metadata[protocol.EntityDataKeyAlwaysShowNameTag]; ok {
if ShowNameTag != 0 {
nbt["CustomNameVisible"] = true
} else {
nbt["CustomNameVisible"] = false
}
}
if _, ok := metadata[protocol.EntityDataKeyFlags]; ok {
if metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagNoAI) {
nbt["IsAutonomous"] = false
}
for k, v := range flagNames {
nbt[v] = metadata.Flag(protocol.EntityDataKeyFlags, k)
}
AlwaysShowName := metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName)
if AlwaysShowName {
nbt["CustomNameVisible"] = true
}
type effect struct {
Id byte
Amplifier byte
Duration int32
DurationEasy int32
DurationNormal int32
DurationHard int32
Ambient bool
ShowParticles bool
DisplayOnScreenTextureAnimation bool
}
activeEffects := []effect{}
addEffect := func(id int, showParticles bool) {
activeEffects = append(activeEffects, effect{
Id: byte(id),
Amplifier: 1,
Duration: 10000000,
ShowParticles: false,
})
}
invisible := metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible)
if invisible {
addEffect(packet.EffectInvisibility, false)
}
if len(activeEffects) > 0 {
nbt["ActiveEffects"] = activeEffects
}
}
}
func vec3float32(x mgl32.Vec3) []float32 {
return []float32{float32(x[0]), float32(x[1]), float32(x[2])}
}
func (s *entityState) ToServerEntity() serverEntity {
e := serverEntity{
EntityType: serverEntityType{
Encoded: s.EntityType,
NBT: map[string]any{
"Pos": vec3float32(s.Position),
"Rotation": []float32{s.HeadYaw, s.Pitch},
"Motion": vec3float32(s.Velocity),
"UniqueID": int64(s.UniqueID),
},
},
}
entityMetadataToNBT(s.Metadata, e.EntityType.NBT)
if s.Helmet != nil || s.Chestplate != nil || s.Leggings != nil || s.Boots != nil {
e.EntityType.NBT["Armor"] = []map[string]any{
nbtconv.WriteItem(stackToItem(s.Helmet.Stack), true),
nbtconv.WriteItem(stackToItem(s.Chestplate.Stack), true),
nbtconv.WriteItem(stackToItem(s.Leggings.Stack), true),
nbtconv.WriteItem(stackToItem(s.Boots.Stack), true),
}
}
return e
}
func (w *worldsHandler) getEntity(id uint64) (*entityState, bool) {
e, ok := w.worldState.entities[id]
return e, ok
}
func (w *worldsHandler) ProcessEntityPackets(pk packet.Packet) packet.Packet {
if !w.settings.SaveEntities {
return pk
}
switch pk := pk.(type) {
case *packet.AddActor:
w.processAddActor(pk)
case *packet.RemoveActor:
case *packet.SetActorData:
e, ok := w.getEntity(pk.EntityRuntimeID)
if ok {
e.Metadata = pk.EntityMetadata
w.bp.AddEntity(behaviourpack.EntityIn{
Identifier: e.EntityType,
Attr: nil,
Meta: pk.EntityMetadata,
})
}
case *packet.SetActorMotion:
e, ok := w.getEntity(pk.EntityRuntimeID)
if ok {
e.Velocity = pk.Velocity
}
case *packet.MoveActorDelta:
e, ok := w.getEntity(pk.EntityRuntimeID)
if ok {
if pk.Flags&packet.MoveActorDeltaFlagHasX != 0 {
e.Position[0] = pk.Position[0]
}
if pk.Flags&packet.MoveActorDeltaFlagHasY != 0 {
e.Position[1] = pk.Position[1]
}
if pk.Flags&packet.MoveActorDeltaFlagHasZ != 0 {
e.Position[2] = pk.Position[2]
}
if pk.Flags&packet.MoveActorDeltaFlagHasRotX != 0 {
e.Pitch = pk.Rotation.X()
}
if pk.Flags&packet.MoveActorDeltaFlagHasRotY != 0 {
e.Yaw = pk.Rotation.Y()
}
//if pk.Flags&packet.MoveActorDeltaFlagHasRotZ != 0 {
// no roll
//}
}
case *packet.MoveActorAbsolute:
e, ok := w.getEntity(pk.EntityRuntimeID)
if ok {
e.Position = pk.Position
e.Pitch = pk.Rotation.X()
e.Yaw = pk.Rotation.Y()
}
case *packet.MobEquipment:
e, ok := w.getEntity(pk.EntityRuntimeID)
if ok {
w, ok := e.Inventory[pk.WindowID]
if !ok {
w = make(map[byte]protocol.ItemInstance)
e.Inventory[pk.WindowID] = w
}
w[pk.HotBarSlot] = pk.NewItem
}
case *packet.MobArmourEquipment:
e, ok := w.getEntity(pk.EntityRuntimeID)
if ok {
e.Helmet = &pk.Helmet
e.Chestplate = &pk.Chestplate
e.Leggings = &pk.Chestplate
e.Boots = &pk.Boots
}
}
return pk
}

183
handlers/worlds/items.go Normal file
View File

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

308
handlers/worlds/map_item.go Normal file
View File

@ -0,0 +1,308 @@
package worlds
import (
"image"
"image/draw"
"math"
"sync"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/go-gl/mathgl/mgl32"
"golang.design/x/lockfree"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
const ViewMapID = 0x424242
// MapItemPacket tells the client that it has a map with id 0x424242 in the offhand
var MapItemPacket = packet.InventoryContent{
WindowID: 119,
Content: []protocol.ItemInstance{
{
StackNetworkID: 1, // random if auth inv
Stack: protocol.ItemStack{
ItemType: protocol.ItemType{
NetworkID: 420, // overwritten in onconnect
MetadataValue: 0,
},
BlockRuntimeID: 0,
Count: 1,
NBTData: map[string]interface{}{
"map_name_index": int64(1),
"map_uuid": int64(ViewMapID),
},
},
},
},
}
func imin(a, b int32) int32 {
if a < b {
return a
}
return b
}
func imax(a, b int32) int32 {
if a > b {
return a
}
return b
}
func (m *MapUI) GetBounds() (min, max protocol.ChunkPos) {
if len(m.renderedChunks) == 0 {
return
}
min = protocol.ChunkPos{math.MaxInt32, math.MaxInt32}
for chunk := range m.renderedChunks {
min[0] = imin(min[0], chunk[0])
min[1] = imin(min[1], chunk[1])
max[0] = imax(max[0], chunk[0])
max[1] = imax(max[1], chunk[1])
}
return
}
type RenderElem struct {
pos protocol.ChunkPos
ch *chunk.Chunk
}
type MapUI struct {
img *image.RGBA // rendered image
zoomLevel int // pixels per chunk
renderQueue *lockfree.Queue
renderedChunks map[protocol.ChunkPos]*image.RGBA // prerendered chunks
needRedraw bool // when the map has updated this is true
showOnGui bool
l sync.RWMutex
ticker *time.Ticker
w *worldsHandler
}
func NewMapUI(w *worldsHandler) *MapUI {
m := &MapUI{
img: image.NewRGBA(image.Rect(0, 0, 128, 128)),
zoomLevel: 16,
renderQueue: lockfree.NewQueue(),
renderedChunks: make(map[protocol.ChunkPos]*image.RGBA),
needRedraw: true,
w: w,
}
return m
}
func (m *MapUI) Start() {
r := m.w.gui.Message(messages.CanShowImages{})
if r.Ok {
m.showOnGui = true
}
// init map
err := m.w.proxy.ClientWritePacket(&packet.ClientBoundMapItemData{
MapID: ViewMapID,
Scale: 4,
MapsIncludedIn: []int64{ViewMapID},
UpdateFlags: packet.MapUpdateFlagInitialisation,
})
if err != nil {
logrus.Error(err)
return
}
m.ticker = time.NewTicker(33 * time.Millisecond)
go func() {
for range m.ticker.C {
if m.needRedraw {
m.needRedraw = false
m.Redraw()
if err := m.w.proxy.ClientWritePacket(&packet.ClientBoundMapItemData{
MapID: ViewMapID,
Scale: 4,
Width: 128,
Height: 128,
Pixels: utils.Img2rgba(m.img),
UpdateFlags: packet.MapUpdateFlagTexture,
}); err != nil {
logrus.Error(err)
return
}
}
}
}()
go func() { // send map item
t := time.NewTicker(1 * time.Second)
for range t.C {
if m.w.ctx.Err() != nil {
return
}
err := m.w.proxy.ClientWritePacket(&MapItemPacket)
if err != nil {
logrus.Error(err)
return
}
}
}()
}
func (m *MapUI) Stop() {
if m.ticker != nil {
m.ticker.Stop()
}
}
// Reset resets the map to inital state
func (m *MapUI) Reset() {
m.l.Lock()
m.renderedChunks = make(map[protocol.ChunkPos]*image.RGBA)
m.l.Unlock()
m.SchedRedraw()
}
// ChangeZoom adds to the zoom value and goes around to 32 once it hits 128
func (m *MapUI) ChangeZoom() {
m.zoomLevel /= 2
if m.zoomLevel == 0 {
m.zoomLevel = 16
}
m.SchedRedraw()
}
// SchedRedraw tells the map to redraw the next time its sent
func (m *MapUI) SchedRedraw() {
m.needRedraw = true
}
// Redraw draws chunk images to the map image
func (m *MapUI) Redraw() {
m.l.Lock()
updatedChunks := make([]protocol.ChunkPos, 0, m.renderQueue.Length())
for {
r, ok := m.renderQueue.Dequeue().(*RenderElem)
if !ok {
break
}
if r.ch != nil {
m.renderedChunks[r.pos] = utils.Chunk2Img(r.ch)
}
updatedChunks = append(updatedChunks, r.pos)
}
m.l.Unlock()
middle := protocol.ChunkPos{
int32(m.w.serverState.PlayerPos.Position.X()),
int32(m.w.serverState.PlayerPos.Position.Z()),
}
chunksPerLine := float64(128 / m.zoomLevel)
pxPerBlock := 128 / chunksPerLine / 16 // how many pixels per block
pxSizeChunk := int(math.Floor(pxPerBlock * 16))
for i := 0; i < len(m.img.Pix); i++ { // clear canvas
m.img.Pix[i] = 0
}
m.l.RLock()
for _ch := range m.renderedChunks {
relativeMiddleX := float64(_ch.X()*16 - middle.X())
relativeMiddleZ := float64(_ch.Z()*16 - middle.Z())
px := image.Point{ // bottom left corner of the chunk on the map
X: int(math.Floor(relativeMiddleX*pxPerBlock)) + 64,
Y: int(math.Floor(relativeMiddleZ*pxPerBlock)) + 64,
}
if !m.img.Rect.Intersect(image.Rect(px.X, px.Y, px.X+pxSizeChunk, px.Y+pxSizeChunk)).Empty() {
utils.DrawImgScaledPos(m.img, m.renderedChunks[_ch], px, pxSizeChunk)
}
}
if m.showOnGui {
min, max := m.GetBounds()
m.w.gui.Message(messages.UpdateMap{
ChunkCount: len(m.renderedChunks),
Rotation: m.w.serverState.PlayerPos.Yaw,
UpdatedTiles: updatedChunks,
Tiles: m.renderedChunks,
BoundsMin: min,
BoundsMax: max,
})
}
m.l.RUnlock()
}
func (m *MapUI) ToImage() *image.RGBA {
// get the chunk coord bounds
min, max := m.GetBounds()
chunksX := int(max[0] - min[0] + 1) // how many chunk lengths is x coordinate
chunksY := int(max[1] - min[1] + 1)
img := image.NewRGBA(image.Rect(0, 0, chunksX*16, chunksY*16))
m.l.RLock()
for pos, tile := range m.renderedChunks {
px := image.Pt(
int((pos.X()-min.X())*16),
int((pos.Z()-min.Z())*16),
)
draw.Draw(img, image.Rect(
px.X, px.Y,
px.X+16, px.Y+16,
), tile, image.Point{}, draw.Src)
}
m.l.RUnlock()
return img
}
func (m *MapUI) SetChunk(pos world.ChunkPos, ch *chunk.Chunk, complete bool) {
m.renderQueue.Enqueue(&RenderElem{(protocol.ChunkPos)(pos), ch})
m.SchedRedraw()
}
func (w *worldsHandler) ProcessAnimate(pk *packet.Animate) {
if pk.ActionType == packet.AnimateActionSwingArm {
w.mapUI.ChangeZoom()
w.proxy.SendPopup(locale.Loc("zoom_level", locale.Strmap{"Level": w.mapUI.zoomLevel}))
}
}
func (w *worldsHandler) SetPlayerPos(Position mgl32.Vec3, Pitch, Yaw, HeadYaw float32) {
last := w.serverState.PlayerPos
current := TPlayerPos{
Position: Position,
Pitch: Pitch,
Yaw: Yaw,
HeadYaw: HeadYaw,
}
w.serverState.PlayerPos = current
if int(last.Position.X()) != int(current.Position.X()) || int(last.Position.Z()) != int(current.Position.Z()) {
w.mapUI.SchedRedraw()
}
}
func (w *worldsHandler) processMapPacketsClient(pk packet.Packet, forward *bool) packet.Packet {
switch pk := pk.(type) {
case *packet.MovePlayer:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.PlayerAuthInput:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.MapInfoRequest:
if pk.MapID == ViewMapID {
w.mapUI.SchedRedraw()
*forward = false
}
case *packet.Animate:
w.ProcessAnimate(pk)
}
return pk
}

202
handlers/worlds/player.go Normal file
View File

@ -0,0 +1,202 @@
package worlds
import (
"github.com/bedrock-tool/bedrocktool/utils/nbtconv"
"github.com/df-mc/dragonfly/server/item/inventory"
)
func (w *worldsHandler) playerData() (ret map[string]any) {
ret = map[string]any{
"format_version": "1.12.0",
"identifier": "minecraft:player",
}
if len(w.serverState.playerInventory) > 0 {
inv := inventory.New(len(w.serverState.playerInventory), nil)
for i, ii := range w.serverState.playerInventory {
inv.SetItem(i, stackToItem(ii.Stack))
}
ret["Inventory"] = nbtconv.InvToNBT(inv)
}
ret["abilities"] = map[string]any{
"doorsandswitches": true,
"op": true,
"opencontainers": true,
"teleport": true,
"attackmobs": true,
"instabuild": true,
"permissionsLevel": int32(3),
"flying": false,
"lightning": false,
"playerPermissionsLevel": int32(2),
"attackplayers": true,
"build": true,
"flySpeed": float32(0.05),
"invulnerable": true,
"mayfly": true,
"mine": true,
"walkSpeed": float32(0.1),
}
type attribute struct {
Name string
Base float32
Current float32
DefaultMax float32
DefaultMin float32
Max float32
Min float32
}
ret["Attributes"] = []attribute{
{
Base: 0,
Current: 0,
DefaultMax: 1024,
DefaultMin: -1024,
Max: 1024,
Min: -1024,
Name: "minecraft:luck",
},
{
Base: 20,
Current: 20,
DefaultMax: 20,
DefaultMin: 0,
Max: 20,
Min: 0,
Name: "minecraft:health",
},
{
Base: 0,
Current: 0,
DefaultMax: 16,
DefaultMin: 0,
Max: 16,
Min: 0,
Name: "minecraft:absorption",
},
{
Base: 0,
Current: 0,
DefaultMax: 1,
DefaultMin: 0,
Max: 1,
Min: 0,
Name: "minecraft:knockback_resistance",
},
{
Base: 0.1,
Current: 0.1,
DefaultMax: 3.4028235e+38,
DefaultMin: 0,
Max: 3.4028235e+38,
Min: 0,
Name: "minecraft:movement",
},
{
Base: 0.02,
Current: 0.02,
DefaultMax: 3.4028235e+38,
DefaultMin: 0,
Max: 3.4028235e+38,
Min: 0,
Name: "minecraft:underwater_movement",
},
{
Base: 0.02,
Current: 0.02,
DefaultMax: 3.4028235e+38,
DefaultMin: 0,
Max: 3.4028235e+38,
Min: 0,
Name: "minecraft:lava_movement",
},
{
Base: 16,
Current: 16,
DefaultMax: 2048,
DefaultMin: 0,
Max: 2048,
Min: 0,
Name: "minecraft:follow_range",
},
{
Base: 1,
Current: 1,
DefaultMax: 1,
DefaultMin: 1,
Max: 1,
Min: 1,
Name: "minecraft:attack_damage",
},
{
Base: 20,
Current: 20,
DefaultMax: 20,
DefaultMin: 0,
Max: 20,
Min: 0,
Name: "minecraft:player.hunger",
},
{
Base: 0,
Current: 0,
DefaultMax: 20,
DefaultMin: 0,
Max: 20,
Min: 0,
Name: "minecraft:player.exhaustion",
},
{
Base: 5,
Current: 5,
DefaultMax: 20,
DefaultMin: 0,
Max: 20,
Min: 0,
Name: "minecraft:player.saturation",
},
{
Base: 0,
Current: 0,
DefaultMax: 24791,
DefaultMin: 0,
Max: 24791,
Min: 0,
Name: "minecraft:player.level",
},
{
Base: 0,
Current: 0,
DefaultMax: 1,
DefaultMin: 0,
Max: 1,
Min: 0,
Name: "minecraft:player.experience",
},
}
ret["Tags"] = []string{}
ret["OnGround"] = true
spawn := w.serverState.PlayerPos.Position
ret["SpawnX"] = int32(spawn.X())
ret["SpawnY"] = int32(spawn.Y())
ret["SpawnZ"] = int32(spawn.Z())
ret["Pos"] = []float32{
float32(spawn.X()),
float32(spawn.Y()),
float32(spawn.Z()),
}
ret["Rotation"] = []float32{
w.serverState.PlayerPos.Pitch,
w.serverState.PlayerPos.Yaw,
}
return
}

628
handlers/worlds/world.go Normal file
View File

@ -0,0 +1,628 @@
package worlds
import (
"context"
"encoding/json"
"fmt"
"image"
"image/color"
"image/png"
"math/rand"
"os"
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/bedrock-tool/bedrocktool/utils/behaviourpack"
"github.com/flytam/filenamify"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/df-mc/dragonfly/server/world/mcdb"
"github.com/df-mc/goleveldb/leveldb/opt"
"github.com/go-gl/mathgl/mgl32"
"github.com/repeale/fp-go"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type TPlayerPos struct {
Position mgl32.Vec3
Pitch float32
Yaw float32
HeadYaw float32
}
// the state used for drawing and saving
type WorldSettings struct {
// settings
VoidGen bool
WithPacks bool
SaveImage bool
SaveEntities bool
SaveInventories bool
BlockUpdates bool
}
type worldState struct {
dimension world.Dimension
chunks map[world.ChunkPos]*chunk.Chunk
blockNBTs map[cube.Pos]map[string]any
entities map[uint64]*entityState
openItemContainers map[byte]*itemContainer
Name string
}
type serverState struct {
ChunkRadius int
ispre118 bool
worldCounter int
playerInventory []protocol.ItemInstance
PlayerPos TPlayerPos
Name string
}
type worldsHandler struct {
ctx context.Context
wg sync.WaitGroup
proxy *utils.ProxyContext
mapUI *MapUI
gui utils.UI
bp *behaviourpack.BehaviourPack
worldState worldState
serverState serverState
settings WorldSettings
}
func NewWorldsHandler(ctx context.Context, ui utils.UI, settings WorldSettings) *utils.ProxyHandler {
w := &worldsHandler{
ctx: ctx,
gui: ui,
serverState: serverState{
ispre118: false,
worldCounter: 0,
ChunkRadius: 0,
PlayerPos: TPlayerPos{},
},
settings: settings,
}
w.mapUI = NewMapUI(w)
w.Reset()
return &utils.ProxyHandler{
Name: "Worlds",
ProxyRef: func(pc *utils.ProxyContext) {
w.proxy = pc
w.proxy.AddCommand(utils.IngameCommand{
Exec: func(cmdline []string) bool {
return w.setWorldName(strings.Join(cmdline, " "), false)
},
Cmd: protocol.Command{
Name: "setname",
Description: locale.Loc("setname_desc", nil),
Overloads: []protocol.CommandOverload{{
Parameters: []protocol.CommandParameter{{
Name: "name",
Type: protocol.CommandArgTypeString,
Optional: false,
}},
}},
},
})
w.proxy.AddCommand(utils.IngameCommand{
Exec: func(cmdline []string) bool {
return w.setVoidGen(!w.settings.VoidGen, false)
},
Cmd: protocol.Command{
Name: "void",
Description: locale.Loc("void_desc", nil),
},
})
},
AddressAndName: func(address, hostname string) error {
w.bp = behaviourpack.New(hostname)
w.serverState.Name = hostname
return nil
},
OnClientConnect: func(conn minecraft.IConn) {
w.gui.Message(messages.SetUIState(messages.UIStateConnecting))
},
GameDataModifier: func(gd *minecraft.GameData) {
gd.ClientSideGeneration = false
},
ConnectCB: w.OnConnect,
PacketCB: func(pk packet.Packet, toServer bool, timeReceived time.Time) (packet.Packet, error) {
forward := true
if toServer {
// from client
pk = w.processItemPacketsClient(pk, &forward)
pk = w.processMapPacketsClient(pk, &forward)
} else {
// from server
switch pk := pk.(type) {
case *packet.ChunkRadiusUpdated:
w.serverState.ChunkRadius = int(pk.ChunkRadius)
pk.ChunkRadius = 80
}
pk = w.processItemPacketsServer(pk)
pk = w.ProcessChunkPackets(pk)
pk = w.ProcessEntityPackets(pk)
}
if !forward {
return nil, nil
}
return pk, nil
},
OnEnd: func() {
w.SaveAndReset()
w.wg.Wait()
},
}
}
func (w *worldsHandler) setVoidGen(val bool, fromUI bool) bool {
w.settings.VoidGen = val
var s = locale.Loc("void_generator_false", nil)
if w.settings.VoidGen {
s = locale.Loc("void_generator_true", nil)
}
w.proxy.SendMessage(s)
if !fromUI {
w.gui.Message(messages.SetVoidGen{
Value: w.settings.VoidGen,
})
}
return true
}
func (w *worldsHandler) setWorldName(val string, fromUI bool) bool {
w.worldState.Name = val
w.proxy.SendMessage(locale.Loc("worldname_set", locale.Strmap{"Name": w.worldState.Name}))
if !fromUI {
w.gui.Message(messages.SetWorldName{
WorldName: w.worldState.Name,
})
}
return true
}
func (w *worldsHandler) currentName() string {
worldName := "world"
if w.serverState.worldCounter > 0 {
worldName = fmt.Sprintf("world-%d", w.serverState.worldCounter)
}
return worldName
}
func (w *worldsHandler) Reset() {
w.worldState = worldState{
dimension: w.worldState.dimension,
chunks: make(map[world.ChunkPos]*chunk.Chunk),
blockNBTs: make(map[cube.Pos]map[string]any),
entities: make(map[uint64]*entityState),
openItemContainers: make(map[byte]*itemContainer),
Name: w.currentName(),
}
w.mapUI.Reset()
}
func (w *worldState) cullChunks() {
keys := make([]world.ChunkPos, 0, len(w.chunks))
for cp := range w.chunks {
keys = append(keys, cp)
}
for _, cp := range fp.Filter(func(cp world.ChunkPos) bool {
return !fp.Some(func(sc *chunk.SubChunk) bool {
return !sc.Empty()
})(w.chunks[cp].Sub())
})(keys) {
delete(w.chunks, cp)
}
}
type dummyBlock struct {
id string
nbt map[string]any
}
func (d *dummyBlock) EncodeBlock() (string, map[string]any) {
return d.id, d.nbt
}
func (d *dummyBlock) Hash() uint64 {
return 0
}
func (d *dummyBlock) Model() world.BlockModel {
return nil
}
func (d *dummyBlock) Color() color.RGBA {
return color.RGBA{0, 0, 0, 0}
}
func (d *dummyBlock) DecodeNBT(data map[string]any) any {
return nil
}
func (d *dummyBlock) EncodeNBT() map[string]any {
return d.nbt
}
func (w *worldState) Save(folder string) (*mcdb.DB, error) {
provider, err := mcdb.Config{
Log: logrus.StandardLogger(),
Compression: opt.DefaultCompression,
}.Open(folder)
if err != nil {
return nil, err
}
chunkBlockNBT := make(map[world.ChunkPos]map[cube.Pos]world.Block)
for bp, blockNBT := range w.blockNBTs { // 3d to 2d
cp := world.ChunkPos{int32(bp.X()) >> 4, int32(bp.Z()) >> 4}
m, ok := chunkBlockNBT[cp]
if !ok {
m = make(map[cube.Pos]world.Block)
chunkBlockNBT[cp] = m
}
id := blockNBT["id"].(string)
m[bp] = &dummyBlock{id, blockNBT}
}
chunkEntities := make(map[world.ChunkPos][]world.Entity)
for _, es := range w.entities {
cp := world.ChunkPos{int32(es.Position.X()) >> 4, int32(es.Position.Z()) >> 4}
chunkEntities[cp] = append(chunkEntities[cp], es.ToServerEntity())
}
// save chunk data
for cp, c := range w.chunks {
column := &world.Column{
Chunk: c,
BlockEntities: chunkBlockNBT[cp],
Entities: chunkEntities[cp],
}
err = provider.StoreColumn(cp, w.dimension, column)
if err != nil {
logrus.Error(err)
}
}
return provider, err
}
func (w *worldsHandler) SaveAndReset() {
w.worldState.cullChunks()
if len(w.worldState.chunks) == 0 {
w.Reset()
return
}
worldStateCopy := w.worldState
playerData := w.playerData()
playerPos := w.serverState.PlayerPos.Position
spawnPos := cube.Pos{int(playerPos.X()), int(playerPos.Y()), int(playerPos.Z())}
var img image.Image
if w.settings.SaveImage {
img = w.mapUI.ToImage()
}
folder := fmt.Sprintf("worlds/%s/%s", w.serverState.Name, worldStateCopy.Name)
filename := folder + ".mcworld"
w.serverState.worldCounter += 1
w.Reset()
w.wg.Add(1)
go func() {
defer w.wg.Done()
logrus.Infof(locale.Loc("saving_world", locale.Strmap{"Name": worldStateCopy.Name, "Count": len(worldStateCopy.chunks)}))
w.gui.Message(messages.SavingWorld{
World: &messages.SavedWorld{
Name: worldStateCopy.Name,
Path: filename,
Chunks: len(worldStateCopy.chunks),
},
})
// open world
os.RemoveAll(folder)
os.MkdirAll(folder, 0o777)
provider, err := worldStateCopy.Save(folder)
if err != nil {
logrus.Error(err)
return
}
err = provider.SaveLocalPlayerData(playerData)
if err != nil {
logrus.Error(err)
return
}
// write metadata
s := provider.Settings()
s.Spawn = spawnPos
s.Name = worldStateCopy.Name
// set gamerules
ld := provider.LevelDat()
gd := w.proxy.Server.GameData()
ld.RandomSeed = int64(gd.WorldSeed)
for _, gr := range gd.GameRules {
switch gr.Name {
case "commandblockoutput":
ld.CommandBlockOutput = gr.Value.(bool)
case "maxcommandchainlength":
ld.MaxCommandChainLength = int32(gr.Value.(uint32))
case "commandblocksenabled":
ld.CommandsEnabled = gr.Value.(bool)
case "dodaylightcycle":
ld.DoDayLightCycle = gr.Value.(bool)
case "doentitydrops":
ld.DoEntityDrops = gr.Value.(bool)
case "dofiretick":
ld.DoFireTick = gr.Value.(bool)
case "domobloot":
ld.DoMobLoot = gr.Value.(bool)
case "domobspawning":
ld.DoMobSpawning = gr.Value.(bool)
case "dotiledrops":
ld.DoTileDrops = gr.Value.(bool)
case "doweathercycle":
ld.DoWeatherCycle = gr.Value.(bool)
case "drowningdamage":
ld.DrowningDamage = gr.Value.(bool)
case "doinsomnia":
ld.DoInsomnia = gr.Value.(bool)
case "falldamage":
ld.FallDamage = gr.Value.(bool)
case "firedamage":
ld.FireDamage = gr.Value.(bool)
case "keepinventory":
ld.KeepInventory = gr.Value.(bool)
case "mobgriefing":
ld.MobGriefing = gr.Value.(bool)
case "pvp":
ld.PVP = gr.Value.(bool)
case "showcoordinates":
ld.ShowCoordinates = gr.Value.(bool)
case "naturalregeneration":
ld.NaturalRegeneration = gr.Value.(bool)
case "tntexplodes":
ld.TNTExplodes = gr.Value.(bool)
case "sendcommandfeedback":
ld.SendCommandFeedback = gr.Value.(bool)
case "randomtickspeed":
ld.RandomTickSpeed = int32(gr.Value.(uint32))
case "doimmediaterespawn":
ld.DoImmediateRespawn = gr.Value.(bool)
case "showdeathmessages":
ld.ShowDeathMessages = gr.Value.(bool)
case "functioncommandlimit":
ld.FunctionCommandLimit = int32(gr.Value.(uint32))
case "spawnradius":
ld.SpawnRadius = int32(gr.Value.(uint32))
case "showtags":
ld.ShowTags = gr.Value.(bool)
case "freezedamage":
ld.FreezeDamage = gr.Value.(bool)
case "respawnblocksexplode":
ld.RespawnBlocksExplode = gr.Value.(bool)
case "showbordereffect":
ld.ShowBorderEffect = gr.Value.(bool)
// todo
default:
logrus.Warnf(locale.Loc("unknown_gamerule", locale.Strmap{"Name": gr.Name}))
}
}
// void world
if w.settings.VoidGen {
ld.FlatWorldLayers = `{"biome_id":1,"block_layers":[{"block_data":0,"block_id":0,"count":1},{"block_data":0,"block_id":0,"count":2},{"block_data":0,"block_id":0,"count":1}],"encoding_version":3,"structure_options":null}`
ld.Generator = 2
}
ld.RandomTickSpeed = 0
s.CurrentTick = 0
if w.bp.HasContent() {
if ld.Experiments == nil {
ld.Experiments = map[string]any{}
}
ld.Experiments["data_driven_items"] = true
ld.Experiments["experiments_ever_used"] = true
ld.Experiments["saved_with_toggled_experiments"] = true
}
provider.SaveSettings(s)
err = provider.Close()
if err != nil {
logrus.Error(err)
return
}
if w.settings.SaveImage {
f, _ := os.Create(folder + ".png")
png.Encode(f, img)
f.Close()
}
w.AddPacks(folder)
// zip it
err = utils.ZipFolder(filename, folder)
if err != nil {
logrus.Error(err)
return
}
logrus.Info(locale.Loc("saved", locale.Strmap{"Name": filename}))
//os.RemoveAll(folder)
w.gui.Message(messages.SetUIState(messages.UIStateMain))
}()
}
func (w *worldsHandler) AddPacks(folder string) {
type dep struct {
PackID string `json:"pack_id"`
Version [3]int `json:"version"`
}
addPacksJSON := func(name string, deps []dep) {
f, err := os.Create(path.Join(folder, name))
if err != nil {
logrus.Error(err)
return
}
defer f.Close()
if err := json.NewEncoder(f).Encode(deps); err != nil {
logrus.Error(err)
return
}
}
// save behaviourpack
if w.bp.HasContent() {
name := strings.ReplaceAll(w.serverState.Name, "./", "")
name = strings.ReplaceAll(name, "/", "-")
name = strings.ReplaceAll(name, ":", "_")
packFolder := path.Join(folder, "behavior_packs", name)
os.MkdirAll(packFolder, 0o755)
for _, p := range w.proxy.Server.ResourcePacks() {
p := utils.PackFromBase(p)
w.bp.CheckAddLink(p)
}
w.bp.Save(packFolder)
addPacksJSON("world_behavior_packs.json", []dep{{
PackID: w.bp.Manifest.Header.UUID,
Version: w.bp.Manifest.Header.Version,
}})
// force resource packs for worlds with custom blocks
w.settings.WithPacks = true
}
// add resource packs
if w.settings.WithPacks {
packs, err := utils.GetPacks(w.proxy.Server)
if err != nil {
logrus.Error(err)
} else {
packNames := make(map[string]int)
for _, pack := range packs {
packNames[pack.Name()] += 1
}
var rdeps []dep
for _, pack := range packs {
if pack.Encrypted() && !pack.CanDecrypt() {
logrus.Warnf("Cant add %s, it is encrypted", pack.Name())
continue
}
logrus.Infof(locale.Loc("adding_pack", locale.Strmap{"Name": pack.Name()}))
packName := pack.Name()
if packNames[packName] > 1 {
packName += "_" + pack.UUID()
}
packName, _ = filenamify.FilenamifyV2(packName)
packFolder := path.Join(folder, "resource_packs", packName)
os.MkdirAll(packFolder, 0o755)
utils.UnpackZip(pack, int64(pack.Len()), packFolder)
rdeps = append(rdeps, dep{
PackID: pack.Manifest().Header.UUID,
Version: pack.Manifest().Header.Version,
})
}
if len(rdeps) > 0 {
addPacksJSON("world_resource_packs.json", rdeps)
}
}
}
}
func (w *worldsHandler) OnConnect(err error) bool {
w.gui.Message(messages.SetWorldName{
WorldName: w.worldState.Name,
})
w.gui.Message(messages.SetUIState(messages.UIStateMain))
if err != nil {
return false
}
gd := w.proxy.Server.GameData()
w.serverState.ChunkRadius = int(gd.ChunkRadius)
w.proxy.ClientWritePacket(&packet.ChunkRadiusUpdated{
ChunkRadius: 80,
})
world.InsertCustomItems(gd.Items)
for _, ie := range gd.Items {
w.bp.AddItem(ie)
}
mapItemID, _ := world.ItemRidByName("minecraft:filled_map")
MapItemPacket.Content[0].Stack.ItemType.NetworkID = mapItemID
if gd.ServerAuthoritativeInventory {
MapItemPacket.Content[0].StackNetworkID = 0xffff + rand.Int31n(0xfff)
}
if len(gd.CustomBlocks) > 0 {
logrus.Info(locale.Loc("using_customblocks", nil))
for _, be := range gd.CustomBlocks {
w.bp.AddBlock(be)
}
// telling the chunk code what custom blocks there are so it can generate offsets
world.InsertCustomBlocks(gd.CustomBlocks)
}
{ // check game version
gv := strings.Split(gd.BaseGameVersion, ".")
var err error
if len(gv) > 1 {
var ver int
ver, err = strconv.Atoi(gv[1])
w.serverState.ispre118 = ver < 18
}
if err != nil || len(gv) <= 1 {
logrus.Info(locale.Loc("guessing_version", nil))
}
dimensionID := gd.Dimension
if w.serverState.ispre118 {
logrus.Info(locale.Loc("using_under_118", nil))
if dimensionID == 0 {
dimensionID += 10
}
}
w.worldState.dimension, _ = world.DimensionByID(int(dimensionID))
}
w.proxy.SendMessage(locale.Loc("use_setname", nil))
w.mapUI.Start()
return true
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

200
locale/de.yaml Normal file
View File

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

201
locale/en.yaml Normal file
View File

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

83
locale/i18n.go Normal file
View File

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

202
locale/owo.yaml Normal file
View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -3,61 +3,36 @@ package subcommands
import (
"context"
"flag"
"strings"
seconduser "github.com/bedrock-tool/bedrocktool/handlers/second-user"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/subcommands"
"github.com/sirupsen/logrus"
)
type DebugProxyCMD struct {
Address string
filter string
ServerAddress string
}
func (*DebugProxyCMD) Name() string { return "debug-proxy" }
func (*DebugProxyCMD) Synopsis() string { return "verbose debug packets" }
func (*DebugProxyCMD) Synopsis() string { return locale.Loc("debug_proxy_synopsis", nil) }
func (c *DebugProxyCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.Address, "address", "", "remote server address")
f.StringVar(&c.filter, "filter", "", "packets to not show")
f.StringVar(&c.ServerAddress, "address", "", locale.Loc("remote_address", nil))
}
func (c *DebugProxyCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n" + utils.SERVER_ADDRESS_HELP
}
func (c *DebugProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
address, _, err := utils.ServerInput(ctx, c.Address)
func (c *DebugProxyCMD) Execute(ctx context.Context, ui utils.UI) error {
address, hostname, err := utils.ServerInput(ctx, c.ServerAddress)
if err != nil {
logrus.Error(err)
return 1
return err
}
utils.G_debug = true
utils.Options.Debug = true
filters := strings.Split(c.filter, ",")
if len(filters) > 0 {
for _, v := range filters {
if len(v) == 0 {
continue
}
if string(v[0]) == "*" {
v = v[1:]
}
v = strings.TrimPrefix(v, "packet.")
v = "packet." + v
utils.ExtraVerbose = append(utils.ExtraVerbose, v)
}
proxy, err := utils.NewProxy()
if err != nil {
return err
}
proxy := utils.NewProxy(logrus.StandardLogger())
if err := proxy.Run(ctx, address); err != nil {
logrus.Error(err)
return 1
}
return 0
proxy.AddHandler(seconduser.NewSecondUser())
return proxy.Run(ctx, address, hostname)
}
func init() {

View File

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

View File

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

View File

@ -0,0 +1,30 @@
package subcommands
import (
"context"
"flag"
"fmt"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
)
type RealmListCMD struct{}
func (*RealmListCMD) Name() string { return "list-realms" }
func (*RealmListCMD) Synopsis() string { return locale.Loc("list_realms_synopsis", nil) }
func (c *RealmListCMD) SetFlags(f *flag.FlagSet) {}
func (c *RealmListCMD) Execute(ctx context.Context, ui utils.UI) error {
realms, err := utils.GetRealmsAPI().Realms(ctx)
if err != nil {
return err
}
for _, realm := range realms {
fmt.Println(locale.Loc("realm_list_line", locale.Strmap{"Name": realm.Name, "Id": realm.ID}))
}
return nil
}
func init() {
utils.RegisterCommand(&RealmListCMD{})
}

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

View File

@ -1,99 +0,0 @@
package world
import (
"image"
"image/color"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
)
func blockColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) (blockColor color.RGBA) {
blockColor = color.RGBA{255, 0, 255, 255}
rid := c.Block(x, y, z, 0)
if rid == 0 && y == 0 { // void
blockColor = color.RGBA{0, 0, 0, 255}
} else {
b, found := world.BlockByRuntimeID(rid)
if found {
if _, ok := b.(block.Water); ok {
y2 := c.HeightMap().At(x, z)
depth := y - y2
if depth > 0 {
blockColor = blockColorAt(c, x, y2, z)
}
bw := (&block.Water{}).Color()
bw.A = uint8(utils.Clamp(int(150+depth*7), 255))
blockColor = utils.BlendColors(blockColor, bw)
blockColor.R -= uint8(depth * 2)
blockColor.G -= uint8(depth * 2)
blockColor.B -= uint8(depth * 2)
} else {
blockColor = b.Color()
}
}
/*
if blockColor.R == 0 || blockColor.R == 255 && blockColor.B == 255 {
name, nbt := b.EncodeBlock()
fmt.Printf("unknown color %d %s %s %s\n", rid, reflect.TypeOf(b), name, nbt)
b.Color()
}
*/
}
return blockColor
}
func chunkGetColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) color.RGBA {
p := cube.Pos{int(x), int(y), int(z)}
have_up := false
p.Side(cube.FaceUp).Neighbours(func(neighbour cube.Pos) {
if neighbour.X() < 0 || neighbour.X() >= 16 || neighbour.Z() < 0 || neighbour.Z() >= 16 || neighbour.Y() > c.Range().Max() {
return
}
if !have_up {
block_rid := c.Block(uint8(neighbour[0]), int16(neighbour[1]), uint8(neighbour[2]), 0)
if block_rid > 0 {
b, found := world.BlockByRuntimeID(block_rid)
if found {
if _, ok := b.(block.Air); !ok {
have_up = true
}
}
}
}
}, cube.Range{int(y + 1), int(y + 1)})
col := blockColorAt(c, x, y, z)
if have_up {
if col.R > 10 {
col.R -= 10
}
if col.G > 10 {
col.G -= 10
}
if col.B > 10 {
col.B -= 10
}
}
return col
}
func Chunk2Img(c *chunk.Chunk) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, 16, 16))
hm := c.HeightMapWithWater()
for x := uint8(0); x < 16; x++ {
for z := uint8(0); z < 16; z++ {
height := hm.At(x, z)
col := chunkGetColorAt(c, x, height, z)
img.SetRGBA(int(x), int(z), col)
}
}
return img
}

View File

@ -1,237 +0,0 @@
package world
import (
"bytes"
"image"
"image/draw"
"math"
"os"
"time"
"github.com/bedrock-tool/bedrocktool/utils"
"golang.design/x/lockfree"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
"golang.org/x/image/bmp"
)
const VIEW_MAP_ID = 0x424242
// packet to tell the client that it has a map with id 0x424242 in the offhand
var MAP_ITEM_PACKET packet.InventoryContent = packet.InventoryContent{
WindowID: 119,
Content: []protocol.ItemInstance{
{
StackNetworkID: 1,
Stack: protocol.ItemStack{
ItemType: protocol.ItemType{
NetworkID: 420,
MetadataValue: 0,
},
BlockRuntimeID: 0,
Count: 1,
NBTData: map[string]interface{}{
"map_uuid": int64(VIEW_MAP_ID),
},
},
},
},
}
func (m *MapUI) get_bounds() (min, max protocol.ChunkPos) {
// get the chunk coord bounds
for _ch := range m.renderedChunks {
if _ch.X() < min.X() {
min[0] = _ch.X()
}
if _ch.Z() < min.Z() {
min[1] = _ch.Z()
}
if _ch.X() > max.X() {
max[0] = _ch.X()
}
if _ch.Z() > max.Z() {
max[1] = _ch.Z()
}
}
return
}
type RenderElem struct {
pos protocol.ChunkPos
ch *chunk.Chunk
}
type MapUI struct {
img *image.RGBA // rendered image
zoomLevel int // pixels per chunk
renderQueue *lockfree.Queue
renderedChunks map[protocol.ChunkPos]*image.RGBA // prerendered chunks
needRedraw bool // when the map has updated this is true
ticker *time.Ticker
w *WorldState
}
func NewMapUI(w *WorldState) *MapUI {
m := &MapUI{
img: image.NewRGBA(image.Rect(0, 0, 128, 128)),
zoomLevel: 16,
renderQueue: lockfree.NewQueue(),
renderedChunks: make(map[protocol.ChunkPos]*image.RGBA),
needRedraw: true,
w: w,
}
return m
}
func (m *MapUI) Start() {
m.ticker = time.NewTicker(33 * time.Millisecond)
go func() {
for range m.ticker.C {
if m.needRedraw {
m.needRedraw = false
m.Redraw()
if m.w.proxy.Client != nil {
if err := m.w.proxy.Client.WritePacket(&packet.ClientBoundMapItemData{
MapID: VIEW_MAP_ID,
Width: 128,
Height: 128,
Pixels: utils.Img2rgba(m.img),
UpdateFlags: 2,
}); err != nil {
logrus.Error(err)
return
}
}
}
}
}()
}
func (m *MapUI) Stop() {
if m.ticker != nil {
m.ticker.Stop()
}
}
// Reset resets the map to inital state
func (m *MapUI) Reset() {
m.renderedChunks = make(map[protocol.ChunkPos]*image.RGBA)
m.SchedRedraw()
}
// ChangeZoom adds to the zoom value and goes around to 32 once it hits 128
func (m *MapUI) ChangeZoom() {
m.zoomLevel /= 2
if m.zoomLevel == 0 {
m.zoomLevel = 16
}
m.SchedRedraw()
}
// SchedRedraw tells the map to redraw the next time its sent
func (m *MapUI) SchedRedraw() {
m.needRedraw = true
}
// draw_img_scaled_pos draws src onto dst at bottom_left, scaled to size
func draw_img_scaled_pos(dst *image.RGBA, src *image.RGBA, bottom_left image.Point, size_scaled int) {
sbx := src.Bounds().Dx()
ratio := int(float64(sbx) / float64(size_scaled))
for x_out := bottom_left.X; x_out < bottom_left.X+size_scaled; x_out++ {
for y_out := bottom_left.Y; y_out < bottom_left.Y+size_scaled; y_out++ {
x_in := (x_out - bottom_left.X) * ratio
y_in := (y_out - bottom_left.Y) * ratio
c := src.At(x_in, y_in)
dst.Set(x_out, y_out, c)
}
}
}
// draw chunk images to the map image
func (m *MapUI) Redraw() {
for {
r, ok := m.renderQueue.Dequeue().(*RenderElem)
if !ok {
break
}
if r.ch != nil {
m.renderedChunks[r.pos] = Chunk2Img(r.ch)
} else {
m.renderedChunks[r.pos] = black_16x16
}
}
middle := protocol.ChunkPos{
int32(m.w.PlayerPos.Position.X()),
int32(m.w.PlayerPos.Position.Z()),
}
// total_width := 32 * math.Ceil(float64(chunks_x)/32)
chunks_per_line := float64(128 / m.zoomLevel)
px_per_block := 128 / chunks_per_line / 16 // how many pixels per block
sz_chunk := int(math.Floor(px_per_block * 16))
for i := 0; i < len(m.img.Pix); i++ { // clear canvas
m.img.Pix[i] = 0
}
for _ch := range m.renderedChunks {
relative_middle_x := float64(_ch.X()*16 - middle.X())
relative_middle_z := float64(_ch.Z()*16 - middle.Z())
px_pos := image.Point{ // bottom left corner of the chunk on the map
X: int(math.Floor(relative_middle_x*px_per_block)) + 64,
Y: int(math.Floor(relative_middle_z*px_per_block)) + 64,
}
if !m.img.Rect.Intersect(image.Rect(px_pos.X, px_pos.Y, px_pos.X+sz_chunk, px_pos.Y+sz_chunk)).Empty() {
draw_img_scaled_pos(m.img, m.renderedChunks[_ch], px_pos, sz_chunk)
}
}
draw_full := false
if draw_full {
img2 := m.ToImage()
buf := bytes.NewBuffer(nil)
bmp.Encode(buf, img2)
os.WriteFile("test.bmp", buf.Bytes(), 0o777)
}
}
func (m *MapUI) ToImage() *image.RGBA {
// get the chunk coord bounds
min, max := m.get_bounds()
chunks_x := int(max[0] - min[0] + 1) // how many chunk lengths is x coordinate
chunks_y := int(max[1] - min[1] + 1)
img2 := image.NewRGBA(image.Rect(0, 0, chunks_x*16, chunks_y*16))
middle_block_x := chunks_x / 2 * 16
middle_block_y := chunks_y / 2 * 16
for pos := range m.renderedChunks {
px_pos := image.Point{
X: int(pos.X()*16) - middle_block_x + img2.Rect.Dx(),
Y: int(pos.Z()*16) - middle_block_y + img2.Rect.Dy(),
}
draw.Draw(img2, image.Rect(
px_pos.X,
px_pos.Y,
px_pos.X+16,
px_pos.Y+16,
), m.renderedChunks[pos], image.Point{}, draw.Src)
}
return img2
}
func (m *MapUI) SetChunk(pos protocol.ChunkPos, ch *chunk.Chunk) {
m.renderQueue.Enqueue(&RenderElem{pos, ch})
m.SchedRedraw()
}

View File

@ -1,675 +1,65 @@
package world
import (
"bytes"
"context"
"flag"
"fmt"
"image"
"image/draw"
"image/png"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/bedrock-tool/bedrocktool/handlers/worlds"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/bedrock-tool/bedrocktool/utils/nbtconv"
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/df-mc/dragonfly/server/world/mcdb"
"github.com/df-mc/goleveldb/leveldb/opt"
"github.com/go-gl/mathgl/mgl32"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
//_ "github.com/df-mc/dragonfly/server/block" // to load blocks
//_ "net/http/pprof"
)
type TPlayerPos struct {
Position mgl32.Vec3
Pitch float32
Yaw float32
HeadYaw float32
}
type itemContainer struct {
OpenPacket *packet.ContainerOpen
Content *packet.InventoryContent
}
// the state used for drawing and saving
type WorldState struct {
ctx context.Context
ispre118 bool
voidgen bool
chunks map[protocol.ChunkPos]*chunk.Chunk
blockNBT map[protocol.SubChunkPos][]map[string]any
openItemContainers map[byte]*itemContainer
airRid uint32
Dim world.Dimension
WorldName string
ServerName string
worldCounter int
packs map[string]*resource.Pack
withPacks bool
saveImage bool
experimentInventory bool
PlayerPos TPlayerPos
proxy *utils.ProxyContext
// ui
ui *MapUI
}
func NewWorldState() *WorldState {
w := &WorldState{
chunks: make(map[protocol.ChunkPos]*chunk.Chunk),
blockNBT: make(map[protocol.SubChunkPos][]map[string]any),
openItemContainers: make(map[byte]*itemContainer),
Dim: nil,
WorldName: "world",
PlayerPos: TPlayerPos{},
airRid: 6692,
}
w.ui = NewMapUI(w)
return w
}
var dimension_ids = map[uint8]world.Dimension{
0: world.Overworld,
1: world.Nether,
2: world.End,
// < 1.18
10: world.Overworld_legacy,
11: world.Nether,
12: world.End,
}
var (
black_16x16 = image.NewRGBA(image.Rect(0, 0, 16, 16))
Offset_table [24]protocol.SubChunkOffset
)
func init() {
for i := range Offset_table {
Offset_table[i] = protocol.SubChunkOffset{0, int8(i), 0}
}
draw.Draw(black_16x16, image.Rect(0, 0, 16, 16), image.Black, image.Point{}, draw.Src)
utils.RegisterCommand(&WorldCMD{})
}
type WorldCMD struct {
Address string
packs bool
enableVoid bool
saveImage bool
experimentInventory bool
ServerAddress string
Packs bool
EnableVoid bool
SaveEntities bool
SaveInventories bool
SaveImage bool
}
func (*WorldCMD) Name() string { return "worlds" }
func (*WorldCMD) Synopsis() string { return "download a world from a server" }
func (*WorldCMD) Synopsis() string { return locale.Loc("world_synopsis", nil) }
func (p *WorldCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&p.Address, "address", "", "remote server address")
f.BoolVar(&p.packs, "packs", false, "save resourcepacks to the worlds")
f.BoolVar(&p.enableVoid, "void", true, "if false, saves with default flat generator")
f.BoolVar(&p.saveImage, "image", false, "saves an png of the map at the end")
f.BoolVar(&p.experimentInventory, "inv", false, "enable experimental block inventory saving")
func (c *WorldCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.ServerAddress, "address", "", locale.Loc("remote_address", nil))
f.BoolVar(&c.Packs, "packs", false, locale.Loc("save_packs_with_world", nil))
f.BoolVar(&c.EnableVoid, "void", true, locale.Loc("enable_void", nil))
f.BoolVar(&c.SaveImage, "image", false, locale.Loc("save_image", nil))
f.BoolVar(&c.SaveEntities, "save-entities", true, "Save Entities")
f.BoolVar(&c.SaveInventories, "save-inventories", true, "Save Inventories")
}
func (c *WorldCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n" + utils.SERVER_ADDRESS_HELP
}
func (c *WorldCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
/*
go func() {
http.ListenAndServe(":8000", nil)
}()
*/
server_address, hostname, err := utils.ServerInput(ctx, c.Address)
func (c *WorldCMD) Execute(ctx context.Context, ui utils.UI) error {
serverAddress, hostname, err := ui.ServerInput(ctx, c.ServerAddress)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
return err
}
w := NewWorldState()
w.voidgen = c.enableVoid
w.ServerName = hostname
w.withPacks = c.packs
w.saveImage = c.saveImage
w.experimentInventory = c.experimentInventory
w.ctx = ctx
proxy := utils.NewProxy(logrus.StandardLogger())
proxy.ConnectCB = w.OnConnect
proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool) (packet.Packet, error) {
var forward bool
if toServer {
pk, forward = w.ProcessPacketClient(pk)
} else {
pk, forward = w.ProcessPacketServer(pk)
}
if !forward {
return nil, nil
}
return pk, nil
}
err = proxy.Run(ctx, server_address)
proxy, err := utils.NewProxy()
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
w.SaveAndReset()
return 0
}
func (w *WorldState) setnameCommand(cmdline []string) bool {
w.WorldName = strings.Join(cmdline, " ")
w.proxy.SendMessage(fmt.Sprintf("worldName is now: %s", w.WorldName))
return true
}
func (w *WorldState) toggleVoid(cmdline []string) bool {
w.voidgen = !w.voidgen
w.proxy.SendMessage(fmt.Sprintf("using void generator: %t", w.voidgen))
return true
}
func (w *WorldState) ProcessLevelChunk(pk *packet.LevelChunk) {
_, exists := w.chunks[pk.Position]
if exists {
return
return err
}
ch, blockNBTs, err := chunk.NetworkDecode(w.airRid, pk.RawPayload, int(pk.SubChunkCount), w.Dim.Range(), w.ispre118)
proxy.AlwaysGetPacks = true
proxy.AddHandler(worlds.NewWorldsHandler(ctx, ui, worlds.WorldSettings{
VoidGen: c.EnableVoid,
WithPacks: c.Packs,
SaveEntities: c.SaveEntities,
SaveInventories: c.SaveInventories,
SaveImage: c.SaveImage,
}))
ui.Message(messages.SetUIState(messages.UIStateConnect))
err = proxy.Run(ctx, serverAddress, hostname)
if err != nil {
logrus.Error(err)
return
}
if blockNBTs != nil {
w.blockNBT[protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
}] = blockNBTs
}
w.chunks[pk.Position] = ch
if pk.SubChunkRequestMode == protocol.SubChunkRequestModeLegacy {
w.ui.SetChunk(pk.Position, ch)
} else {
w.ui.SetChunk(pk.Position, nil)
// request all the subchunks
max := w.Dim.Range().Height() / 16
if pk.SubChunkRequestMode == protocol.SubChunkRequestModeLimited {
max = int(pk.HighestSubChunk)
}
w.proxy.Server.WritePacket(&packet.SubChunkRequest{
Dimension: int32(w.Dim.EncodeDimension()),
Position: protocol.SubChunkPos{
pk.Position.X(), 0, pk.Position.Z(),
},
Offsets: Offset_table[:max],
})
return err
}
}
func (w *WorldState) ProcessSubChunk(pk *packet.SubChunk) {
pos_to_redraw := make(map[protocol.ChunkPos]bool)
for _, sub := range pk.SubChunkEntries {
var (
abs_x = pk.Position[0] + int32(sub.Offset[0])
abs_y = pk.Position[1] + int32(sub.Offset[1])
abs_z = pk.Position[2] + int32(sub.Offset[2])
subpos = protocol.SubChunkPos{abs_x, abs_y, abs_z}
pos = protocol.ChunkPos{abs_x, abs_z}
)
ch := w.chunks[pos]
if ch == nil {
logrus.Errorf("the server didnt send the chunk before the subchunk!")
continue
}
blockNBT, err := ch.ApplySubChunkEntry(uint8(abs_y), &sub)
if err != nil {
logrus.Error(err)
}
if blockNBT != nil {
w.blockNBT[subpos] = blockNBT
}
pos_to_redraw[pos] = true
}
// redraw the chunks
for pos := range pos_to_redraw {
w.ui.SetChunk(pos, w.chunks[pos])
}
w.ui.SchedRedraw()
}
func (w *WorldState) ProcessAnimate(pk *packet.Animate) {
if pk.ActionType == packet.AnimateActionSwingArm {
w.ui.ChangeZoom()
w.proxy.SendPopup(fmt.Sprintf("Zoom: %d", w.ui.zoomLevel))
}
}
func (w *WorldState) ProcessChangeDimension(pk *packet.ChangeDimension) {
if len(w.chunks) > 0 {
w.SaveAndReset()
} else {
logrus.Info("Skipping save because the world didnt contain any chunks.")
w.Reset()
}
dim_id := pk.Dimension
if w.ispre118 {
dim_id += 10
}
w.Dim = dimension_ids[uint8(dim_id)]
}
func (w *WorldState) SetPlayerPos(Position mgl32.Vec3, Pitch, Yaw, HeadYaw float32) {
last := w.PlayerPos
w.PlayerPos = TPlayerPos{
Position: Position,
Pitch: Pitch,
Yaw: Yaw,
HeadYaw: HeadYaw,
}
if int(last.Position.X()) != int(w.PlayerPos.Position.X()) || int(last.Position.Z()) != int(w.PlayerPos.Position.Z()) {
w.ui.SchedRedraw()
}
}
func (w *WorldState) Reset() {
w.chunks = make(map[protocol.ChunkPos]*chunk.Chunk)
w.WorldName = fmt.Sprintf("world-%d", w.worldCounter)
w.ui.Reset()
}
// writes the world to a folder, resets all the chunks
func (w *WorldState) SaveAndReset() {
logrus.Infof("Saving world %s %d chunks", w.WorldName, len(w.chunks))
// open world
folder := path.Join("worlds", fmt.Sprintf("%s/%s", w.ServerName, w.WorldName))
os.RemoveAll(folder)
os.MkdirAll(folder, 0o777)
provider, err := mcdb.New(logrus.StandardLogger(), folder, opt.DefaultCompression)
if err != nil {
logrus.Fatal(err)
}
// save chunk data
for cp, c := range w.chunks {
provider.SaveChunk((world.ChunkPos)(cp), c, w.Dim)
}
// save block nbt data
blockNBT := make(map[protocol.ChunkPos][]map[string]any)
for scp, v := range w.blockNBT { // 3d to 2d
cp := protocol.ChunkPos{scp.X(), scp.Z()}
blockNBT[cp] = append(blockNBT[cp], v...)
}
for cp, v := range blockNBT {
err = provider.SaveBlockNBT((world.ChunkPos)(cp), v, w.Dim)
if err != nil {
logrus.Error(err)
}
}
// write metadata
s := provider.Settings()
player := w.proxy.Server.GameData().PlayerPosition
s.Spawn = cube.Pos{
int(player.X()),
int(player.Y()),
int(player.Z()),
}
s.Name = w.WorldName
// set gamerules
ld := provider.LevelDat()
gd := w.proxy.Server.GameData()
for _, gr := range gd.GameRules {
switch gr.Name {
case "commandblockoutput":
ld.CommandBlockOutput = gr.Value.(bool)
case "maxcommandchainlength":
ld.MaxCommandChainLength = int32(gr.Value.(uint32))
case "commandblocksenabled":
ld.CommandsEnabled = gr.Value.(bool)
case "dodaylightcycle":
ld.DoDayLightCycle = gr.Value.(bool)
case "doentitydrops":
ld.DoEntityDrops = gr.Value.(bool)
case "dofiretick":
ld.DoFireTick = gr.Value.(bool)
case "domobloot":
ld.DoMobLoot = gr.Value.(bool)
case "domobspawning":
ld.DoMobSpawning = gr.Value.(bool)
case "dotiledrops":
ld.DoTileDrops = gr.Value.(bool)
case "doweathercycle":
ld.DoWeatherCycle = gr.Value.(bool)
case "drowningdamage":
ld.DrowningDamage = gr.Value.(bool)
case "doinsomnia":
ld.DoInsomnia = gr.Value.(bool)
case "falldamage":
ld.FallDamage = gr.Value.(bool)
case "firedamage":
ld.FireDamage = gr.Value.(bool)
case "keepinventory":
ld.KeepInventory = gr.Value.(bool)
case "mobgriefing":
ld.MobGriefing = gr.Value.(bool)
case "pvp":
ld.PVP = gr.Value.(bool)
case "showcoordinates":
ld.ShowCoordinates = gr.Value.(bool)
case "naturalregeneration":
ld.NaturalRegeneration = gr.Value.(bool)
case "tntexplodes":
ld.TNTExplodes = gr.Value.(bool)
case "sendcommandfeedback":
ld.SendCommandFeedback = gr.Value.(bool)
case "randomtickspeed":
ld.RandomTickSpeed = int32(gr.Value.(uint32))
case "doimmediaterespawn":
ld.DoImmediateRespawn = gr.Value.(bool)
case "showdeathmessages":
ld.ShowDeathMessages = gr.Value.(bool)
case "functioncommandlimit":
ld.FunctionCommandLimit = int32(gr.Value.(uint32))
case "spawnradius":
ld.SpawnRadius = int32(gr.Value.(uint32))
case "showtags":
ld.ShowTags = gr.Value.(bool)
case "freezedamage":
ld.FreezeDamage = gr.Value.(bool)
case "respawnblocksexplode":
ld.RespawnBlocksExplode = gr.Value.(bool)
case "showbordereffect":
ld.ShowBorderEffect = gr.Value.(bool)
// todo
default:
logrus.Warnf("unknown gamerule: %s\n", gr.Name)
}
}
ld.RandomSeed = int64(gd.WorldSeed)
// void world
if w.voidgen {
ld.FlatWorldLayers = `{"biome_id":1,"block_layers":[{"block_data":0,"block_id":0,"count":1},{"block_data":0,"block_id":0,"count":2},{"block_data":0,"block_id":0,"count":1}],"encoding_version":3,"structure_options":null}`
ld.Generator = 2
}
provider.SaveSettings(s)
provider.Close()
w.worldCounter += 1
for k, p := range w.packs {
logrus.Infof("Adding resource pack: %s\n", k)
pack_folder := path.Join(folder, "resource_packs", k)
os.MkdirAll(pack_folder, 0o755)
data := make([]byte, p.Len())
p.ReadAt(data, 0)
utils.UnpackZip(bytes.NewReader(data), int64(len(data)), pack_folder)
}
if w.saveImage {
f, _ := os.Create(folder + ".png")
png.Encode(f, w.ui.ToImage())
f.Close()
}
// zip it
filename := folder + ".mcworld"
if err := utils.ZipFolder(filename, folder); err != nil {
fmt.Println(err)
}
logrus.Infof("Saved: %s\n", filename)
os.RemoveAll(folder)
w.Reset()
}
func (w *WorldState) OnConnect(proxy *utils.ProxyContext) {
w.proxy = proxy
gd := w.proxy.Server.GameData()
if len(gd.CustomBlocks) > 0 {
logrus.Info("Using Custom Blocks")
/*
for _, be := range gd.CustomBlocks {
b := block.ServerCustomBlock(be)
world.RegisterBlock(b)
}
*/
}
if w.withPacks {
go func() {
w.packs, _ = utils.GetPacks(w.proxy.Server)
}()
}
{ // check game version
gv := strings.Split(gd.BaseGameVersion, ".")
var err error
if len(gv) > 1 {
var ver int
ver, err = strconv.Atoi(gv[1])
w.ispre118 = ver < 18
}
if err != nil || len(gv) <= 1 {
logrus.Info("couldnt determine game version, assuming > 1.18")
}
dim_id := gd.Dimension
if w.ispre118 {
logrus.Info("using legacy (< 1.18)")
dim_id += 10
}
w.Dim = dimension_ids[uint8(dim_id)]
}
w.proxy.SendMessage("use /setname <worldname>\nto set the world name")
w.ui.Start()
go func() { // send map item
select {
case <-w.ctx.Done():
return
default:
for {
time.Sleep(1 * time.Second)
if w.proxy.Client != nil {
err := w.proxy.Client.WritePacket(&MAP_ITEM_PACKET)
if err != nil {
return
}
}
}
}
}()
proxy.AddCommand(utils.IngameCommand{
Exec: w.setnameCommand,
Cmd: protocol.Command{
Name: "setname",
Description: "set user defined name for this world",
Overloads: []protocol.CommandOverload{
{
Parameters: []protocol.CommandParameter{
{
Name: "name",
Type: protocol.CommandArgTypeString,
Optional: false,
},
},
},
},
},
})
proxy.AddCommand(utils.IngameCommand{
Exec: w.toggleVoid,
Cmd: protocol.Command{
Name: "void",
Description: "toggle if void generator should be used",
},
})
}
func (w *WorldState) ProcessPacketClient(pk packet.Packet) (packet.Packet, bool) {
forward := true
switch pk := pk.(type) {
case *packet.MovePlayer:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.PlayerAuthInput:
w.SetPlayerPos(pk.Position, pk.Pitch, pk.Yaw, pk.HeadYaw)
case *packet.MapInfoRequest:
if pk.MapID == VIEW_MAP_ID {
w.ui.SchedRedraw()
forward = false
}
case *packet.MobEquipment:
if pk.NewItem.Stack.NBTData["map_uuid"] == int64(VIEW_MAP_ID) {
forward = false
}
case *packet.Animate:
w.ProcessAnimate(pk)
}
return pk, forward
}
// stackToItem converts a network ItemStack representation back to an item.Stack.
func stackToItem(it protocol.ItemStack) item.Stack {
t, ok := world.ItemByRuntimeID(it.NetworkID, int16(it.MetadataValue))
if !ok {
t = block.Air{}
}
if it.BlockRuntimeID > 0 {
// It shouldn't matter if it (for whatever reason) wasn't able to get the block runtime ID,
// since on the next line, we assert that the block is an item. If it didn't succeed, it'll
// return air anyway.
b, _ := world.BlockByRuntimeID(uint32(it.BlockRuntimeID))
if t, ok = b.(world.Item); !ok {
t = block.Air{}
}
}
//noinspection SpellCheckingInspection
if nbter, ok := t.(world.NBTer); ok && len(it.NBTData) != 0 {
t = nbter.DecodeNBT(it.NBTData).(world.Item)
}
s := item.NewStack(t, int(it.Count))
return nbtconv.ReadItem(it.NBTData, &s)
}
func (w *WorldState) ProcessPacketServer(pk packet.Packet) (packet.Packet, bool) {
switch pk := pk.(type) {
case *packet.ChangeDimension:
w.ProcessChangeDimension(pk)
case *packet.LevelChunk:
w.ProcessLevelChunk(pk)
w.proxy.SendPopup(fmt.Sprintf("%d chunks loaded\nname: %s", len(w.chunks), w.WorldName))
case *packet.SubChunk:
w.ProcessSubChunk(pk)
case *packet.ContainerOpen:
if w.experimentInventory {
// add to open containers
existing, ok := w.openItemContainers[pk.WindowID]
if !ok {
existing = &itemContainer{}
}
w.openItemContainers[pk.WindowID] = &itemContainer{
OpenPacket: pk,
Content: existing.Content,
}
}
case *packet.InventoryContent:
if w.experimentInventory {
// save content
fmt.Printf("WindowID: %d\n", pk.WindowID)
existing, ok := w.openItemContainers[byte(pk.WindowID)]
if !ok {
if pk.WindowID == 0x0 { // inventory
w.openItemContainers[byte(pk.WindowID)] = &itemContainer{
Content: pk,
}
}
break
}
existing.Content = pk
}
case *packet.ContainerClose:
if w.experimentInventory {
switch pk.WindowID {
case protocol.WindowIDArmour: // todo handle
case protocol.WindowIDOffHand: // todo handle
case protocol.WindowIDUI:
case protocol.WindowIDInventory: // todo handle
default:
// find container info
existing, ok := w.openItemContainers[byte(pk.WindowID)]
if !ok {
logrus.Warn("Closed window that wasnt open")
break
}
if existing.Content == nil {
break
}
pos := existing.OpenPacket.ContainerPosition
cp := protocol.SubChunkPos{pos.X() << 4, pos.Z() << 4}
// create inventory
inv := inventory.New(len(existing.Content.Content), nil)
for i, c := range existing.Content.Content {
item := stackToItem(c.Stack)
inv.SetItem(i, item)
}
// put into subchunk
nbts := w.blockNBT[cp]
for i, v := range nbts {
nbt_pos := protocol.BlockPos{v["x"].(int32), v["y"].(int32), v["z"].(int32)}
if nbt_pos == pos {
w.blockNBT[cp][i]["Items"] = nbtconv.InvToNBT(inv)
}
}
w.proxy.SendMessage("Saved Block Inventory")
// remove it again
delete(w.openItemContainers, byte(pk.WindowID))
}
}
}
return pk, true
ui.Message(messages.SetUIState(messages.UIStateFinished))
return nil
}

142
ui/gui.go Normal file
View File

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

274
ui/gui/pages/packs/packs.go Normal file
View File

@ -0,0 +1,274 @@
package packs
import (
"fmt"
"image"
"image/color"
"sort"
"sync"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/bedrock-tool/bedrocktool/utils"
)
type (
C = layout.Context
D = layout.Dimensions
)
type Page struct {
*pages.Router
State messages.UIState
packsList widget.List
packShowButtons map[string]*widget.Clickable
l sync.Mutex
Packs map[string]*packEntry
}
type packEntry struct {
IsFinished bool
UUID string
HasIcon bool
Icon paint.ImageOp
Size uint64
Loaded uint64
Name string
Path string
Err error
}
func New(router *pages.Router) *Page {
return &Page{
Router: router,
packsList: widget.List{
List: layout.List{
Axis: layout.Vertical,
},
},
Packs: make(map[string]*packEntry),
packShowButtons: make(map[string]*widget.Clickable),
}
}
var _ pages.Page = &Page{}
func (p *Page) Actions() []component.AppBarAction {
return []component.AppBarAction{}
}
func (p *Page) Overflow() []component.OverflowAction {
return []component.OverflowAction{}
}
func (p *Page) NavItem() component.NavItem {
return component.NavItem{
Name: "Pack Download",
//Icon: icon.OtherIcon,
}
}
func drawPackIcon(gtx C, hasImage bool, imageOp paint.ImageOp, bounds image.Point) D {
return layout.Inset{
Top: unit.Dp(5),
Bottom: unit.Dp(5),
Right: unit.Dp(5),
Left: unit.Dp(5),
}.Layout(gtx, func(gtx C) D {
if hasImage {
imageOp.Add(gtx.Ops)
s := imageOp.Size()
p := f32.Pt(float32(s.X), float32(s.Y))
p.X = 1 / (p.X / float32(bounds.X))
p.Y = 1 / (p.Y / float32(bounds.Y))
defer op.Affine(f32.Affine2D{}.Scale(f32.Pt(0, 0), p)).Push(gtx.Ops).Pop()
paint.PaintOp{}.Add(gtx.Ops)
}
return D{Size: bounds}
})
}
func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA {
c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF)
return c
}
func drawPackEntry(gtx C, th *material.Theme, entry *packEntry, button *widget.Clickable) D {
var size = ""
var colorSize = th.Palette.Fg
if entry.IsFinished {
size = utils.SizeofFmt(float32(entry.Size))
} else {
size = fmt.Sprintf("%s / %s %.02f%%",
utils.SizeofFmt(float32(entry.Loaded)),
utils.SizeofFmt(float32(entry.Size)),
float32(entry.Loaded)/float32(entry.Size)*100,
)
colorSize = color.NRGBA{0x00, 0xc9, 0xc9, 0xff}
}
return layout.UniformInset(5).Layout(gtx, func(gtx C) D {
fn := func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return drawPackIcon(gtx, entry.HasIcon, entry.Icon, image.Pt(50, 50))
}),
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Label(th, th.TextSize, entry.Name).Layout),
layout.Rigid(material.LabelStyle{
Text: size,
Color: colorSize,
SelectionColor: MulAlpha(th.Palette.ContrastBg, 0x60),
TextSize: th.TextSize,
Shaper: th.Shaper,
}.Layout),
layout.Rigid(func(gtx C) D {
if entry.Err != nil {
return material.LabelStyle{
Color: color.NRGBA{0xbb, 0x00, 0x00, 0xff},
Text: entry.Err.Error(),
}.Layout(gtx)
}
return D{}
}),
)
}),
)
}
if entry.Path != "" {
return material.ButtonLayoutStyle{
Background: MulAlpha(th.Palette.Bg, 0x60),
Button: button,
CornerRadius: 3,
}.Layout(gtx, fn)
} else {
return fn(gtx)
}
})
}
func (p *Page) layoutFinished(gtx C, th *material.Theme) D {
for uuid, button := range p.packShowButtons {
if button.Clicked() {
pack := p.Packs[uuid]
if pack.IsFinished {
utils.ShowFile(pack.Path)
}
}
}
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Label(th, 20, "Downloaded Packs").Layout),
layout.Flexed(1, func(gtx C) D {
p.l.Lock()
defer p.l.Unlock()
keys := make([]string, 0, len(p.Packs))
for k := range p.Packs {
keys = append(keys, k)
}
sort.Strings(keys)
return material.List(th, &p.packsList).Layout(gtx, len(keys), func(gtx C, index int) D {
entry := p.Packs[keys[index]]
button := p.packShowButtons[keys[index]]
return drawPackEntry(gtx, th, entry, button)
})
}),
)
})
}
func (p *Page) Layout(gtx C, th *material.Theme) D {
margin := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
return margin.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
switch p.State {
case messages.UIStateConnecting:
return layout.Center.Layout(gtx, material.Label(th, 100, "Connecting").Layout)
case messages.UIStateMain:
return p.layoutFinished(gtx, th)
}
return layout.Dimensions{}
})
}
func (p *Page) Handler(data interface{}) messages.MessageResponse {
r := messages.MessageResponse{
Ok: false,
Data: nil,
}
switch m := data.(type) {
case messages.SetUIState:
p.State = m
p.Router.Invalidate()
r.Ok = true
case messages.InitialPacksInfo:
p.State = messages.UIStateMain
p.l.Lock()
for _, dp := range m.Packs {
e := &packEntry{
IsFinished: false,
UUID: dp.UUID,
Name: dp.SubPackName + " v" + dp.Version,
Size: dp.Size,
}
p.Packs[e.UUID] = e
p.packShowButtons[e.UUID] = &widget.Clickable{}
}
p.l.Unlock()
p.Router.Invalidate()
case messages.PackDownloadProgress:
p.l.Lock()
e := p.Packs[m.UUID]
e.Loaded += m.LoadedAdd
if e.Loaded == e.Size {
e.IsFinished = true
}
p.l.Unlock()
p.Router.Invalidate()
case messages.FinishedDownloadingPacks:
p.l.Lock()
for _, dp := range m.Packs {
e := p.Packs[dp.UUID]
if dp.Icon != nil {
e.Icon = paint.NewImageOpFilter(dp.Icon, paint.FilterNearest)
e.HasIcon = true
}
e.Err = dp.Err
e.IsFinished = true
e.Path = dp.Path
}
p.l.Unlock()
p.Router.Invalidate()
r.Ok = true
}
return r
}

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

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

View File

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

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

@ -0,0 +1,117 @@
package skins
import (
"sync"
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages"
"github.com/bedrock-tool/bedrocktool/ui/messages"
)
type (
C = layout.Context
D = layout.Dimensions
)
type Page struct {
*pages.Router
State messages.UIState
SkinsList widget.List
l sync.Mutex
Skins []messages.NewSkin
}
func New(router *pages.Router) *Page {
return &Page{
Router: router,
SkinsList: widget.List{
List: layout.List{
Axis: layout.Vertical,
},
},
}
}
var _ pages.Page = &Page{}
func (p *Page) Actions() []component.AppBarAction {
return []component.AppBarAction{}
}
func (p *Page) Overflow() []component.OverflowAction {
return []component.OverflowAction{}
}
func (p *Page) NavItem() component.NavItem {
return component.NavItem{
Name: "Skin Grabber",
//Icon: icon.OtherIcon,
}
}
func (p *Page) Layout(gtx C, th *material.Theme) D {
margin := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
switch p.State {
case messages.UIStateConnect:
// display login page
return margin.Layout(gtx, material.Label(th, 100, "connect Client").Layout)
case messages.UIStateConnecting:
// display connecting to server
return margin.Layout(gtx, material.Label(th, 100, "Connecting").Layout)
case messages.UIStateMain:
// show the main ui
return margin.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Label(th, 20, "Skin Basic UI").Layout),
layout.Flexed(1, func(gtx C) D {
p.l.Lock()
defer p.l.Unlock()
return material.List(th, &p.SkinsList).Layout(gtx, len(p.Skins), func(gtx C, index int) D {
entry := p.Skins[len(p.Skins)-index-1]
return layout.UniformInset(25).Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(material.Label(th, th.TextSize, entry.PlayerName).Layout),
)
})
})
}),
)
})
}
return layout.Flex{}.Layout(gtx)
}
func (p *Page) Handler(data interface{}) messages.MessageResponse {
r := messages.MessageResponse{
Ok: false,
Data: nil,
}
switch m := data.(type) {
case messages.SetUIState:
p.State = m
p.Router.Invalidate()
r.Ok = true
case messages.NewSkin:
p.l.Lock()
p.Skins = append(p.Skins, m)
p.l.Unlock()
p.Router.Invalidate()
r.Ok = true
}
return r
}

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

@ -0,0 +1,131 @@
package worlds
import (
"image"
"image/draw"
"math"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"github.com/bedrock-tool/bedrocktool/ui/messages"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type Map struct {
click f32.Point
imageOp paint.ImageOp
scaleFactor float32
center f32.Point
transform f32.Affine2D
grabbed bool
cursor image.Point
MapImage *image.RGBA
BoundsMin protocol.ChunkPos
BoundsMax protocol.ChunkPos
}
func (m *Map) HandlePointerEvent(e pointer.Event) {
switch e.Type {
case pointer.Press:
m.click = e.Position
m.grabbed = true
case pointer.Drag:
m.transform = m.transform.Offset(e.Position.Sub(m.click))
m.click = e.Position
case pointer.Release:
m.grabbed = false
case pointer.Scroll:
scaleFactor := -float32(math.Pow(1.01, float64(e.Scroll.Y)))
m.transform = m.transform.Scale(e.Position.Sub(m.center), f32.Pt(scaleFactor, scaleFactor))
m.scaleFactor *= scaleFactor
}
}
func (m *Map) Layout(gtx layout.Context) layout.Dimensions {
m.center = f32.Pt(float32(gtx.Constraints.Max.X), float32(gtx.Constraints.Max.Y)).Div(2)
for _, e := range gtx.Events(m) {
if e, ok := e.(pointer.Event); ok {
m.HandlePointerEvent(e)
}
}
if m.MapImage != nil {
// Calculate the size of the widget based on the size of the image and the current scale factor.
dx := float32(m.MapImage.Bounds().Dx())
dy := float32(m.MapImage.Bounds().Dy())
size := f32.Pt(dx*m.scaleFactor, dy*m.scaleFactor)
// Draw the image at the correct position and scale.
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
op.Affine(m.transform.Offset(m.center.Sub(size.Div(2)))).Add(gtx.Ops)
m.imageOp.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
if m.cursor.In(image.Rectangle(gtx.Constraints)) {
if m.grabbed {
pointer.CursorGrabbing.Add(gtx.Ops)
} else {
pointer.CursorGrab.Add(gtx.Ops)
}
}
}
size := gtx.Constraints.Max
pointer.InputOp{
Tag: m,
Grab: m.grabbed,
Types: pointer.Scroll | pointer.Drag | pointer.Press | pointer.Release,
ScrollBounds: image.Rect(-size.X, -size.Y, size.X, size.Y),
}.Add(gtx.Ops)
return layout.Dimensions{Size: size}
}
func drawTile(img *image.RGBA, min, pos protocol.ChunkPos, tile *image.RGBA) {
px := image.Pt(
int((pos.X()-min[0])*16),
int((pos.Z()-min[1])*16),
)
draw.Draw(img, image.Rect(
px.X, px.Y,
px.X+16, px.Y+16,
), tile, image.Point{}, draw.Src)
}
func (m *Map) Update(u *messages.UpdateMap) {
if m.MapImage == nil {
m.scaleFactor = 1
}
needNewImage := false
if m.BoundsMin != u.BoundsMin {
needNewImage = true
m.BoundsMin = u.BoundsMin
}
if m.BoundsMax != u.BoundsMax {
needNewImage = true
m.BoundsMax = u.BoundsMax
}
if needNewImage {
chunksX := int(m.BoundsMax[0] - m.BoundsMin[0] + 1) // how many chunk lengths is x coordinate
chunksY := int(m.BoundsMax[1] - m.BoundsMin[1] + 1)
m.MapImage = image.NewRGBA(image.Rect(0, 0, chunksX*16, chunksY*16))
for pos, tile := range u.Tiles {
drawTile(m.MapImage, m.BoundsMin, pos, tile)
}
} else {
for _, pos := range u.UpdatedTiles {
tile := u.Tiles[pos]
drawTile(m.MapImage, m.BoundsMin, pos, tile)
}
}
m.imageOp = paint.NewImageOpFilter(m.MapImage, paint.FilterNearest)
}

View File

@ -0,0 +1,157 @@
package worlds
import (
"fmt"
"sync"
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/bedrock-tool/bedrocktool/ui/gui/pages"
"github.com/bedrock-tool/bedrocktool/ui/messages"
)
type (
C = layout.Context
D = layout.Dimensions
)
type Page struct {
*pages.Router
worldMap *Map
State messages.UIState
chunkCount int
voidGen bool
worldName string
worldsList widget.List
worlds []*messages.SavedWorld
l sync.Mutex
}
func New(router *pages.Router) *Page {
return &Page{
Router: router,
worldMap: &Map{},
worldsList: widget.List{
List: layout.List{
Axis: layout.Vertical,
},
},
}
}
var _ pages.Page = &Page{}
func (p *Page) Actions() []component.AppBarAction {
return []component.AppBarAction{}
}
func (p *Page) Overflow() []component.OverflowAction {
return []component.OverflowAction{}
}
func (p *Page) NavItem() component.NavItem {
return component.NavItem{
Name: "World Downloader",
//Icon: icon.OtherIcon,
}
}
func displayWorldEntry(gtx C, th *material.Theme, entry *messages.SavedWorld) D {
return layout.UniformInset(5).Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Label(th, th.TextSize, entry.Name).Layout),
layout.Rigid(material.Label(th, th.TextSize, fmt.Sprintf("%d Chunks", entry.Chunks)).Layout),
)
}),
)
})
}
func (p *Page) Layout(gtx C, th *material.Theme) D {
margin := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
margin.Layout(gtx, func(gtx C) D {
switch p.State {
case messages.UIStateConnect:
// display login page
return layout.Center.Layout(gtx, material.Label(th, 100, "connect Client").Layout)
case messages.UIStateConnecting:
return layout.Center.Layout(gtx, material.Label(th, 100, "Connecting").Layout)
case messages.UIStateMain:
// show the main ui
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
//layout.Rigid(material.Label(th, th.TextSize, p.worldName).Layout),
layout.Flexed(1, func(gtx C) D {
return layout.Center.Layout(gtx, p.worldMap.Layout)
}),
)
case messages.UIStateFinished:
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.UniformInset(15).
Layout(gtx, material.Label(th, 20, "Worlds Saved").Layout)
}),
layout.Flexed(1, func(gtx C) D {
p.l.Lock()
defer p.l.Unlock()
return material.List(th, &p.worldsList).Layout(gtx, len(p.worlds), func(gtx C, index int) D {
entry := p.worlds[len(p.worlds)-index-1]
return displayWorldEntry(gtx, th, entry)
})
}),
)
}
return D{}
})
return layout.Dimensions{}
}
func (u *Page) Handler(data any) messages.MessageResponse {
r := messages.MessageResponse{
Ok: false,
Data: nil,
}
switch m := data.(type) {
case messages.SetUIState:
u.State = m
u.Router.Invalidate()
r.Ok = true
case messages.UpdateMap:
u.chunkCount = m.ChunkCount
u.worldMap.Update(&m)
u.Router.Invalidate()
r.Ok = true
case messages.SetVoidGen:
u.voidGen = m.Value
u.Router.Invalidate()
r.Ok = true
case messages.SetWorldName:
u.worldName = m.WorldName
u.Router.Invalidate()
r.Ok = true
case messages.SavingWorld:
u.l.Lock()
u.worlds = append(u.worlds, m.World)
u.l.Unlock()
u.Router.Invalidate()
r.Ok = true
}
return r
}

34
ui/gui/settings/packs.go Normal file
View File

@ -0,0 +1,34 @@
package settings
import (
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/bedrock-tool/bedrocktool/subcommands"
"github.com/bedrock-tool/bedrocktool/utils"
)
type packsSettings struct {
packs *subcommands.ResourcePackCMD
serverAddress widget.Editor
}
func (s *packsSettings) Init() {
s.packs = utils.ValidCMDs["packs"].(*subcommands.ResourcePackCMD)
s.serverAddress.SingleLine = true
}
func (s *packsSettings) Apply() {
s.packs.ServerAddress = s.serverAddress.Text()
}
func (s *packsSettings) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Editor(th, &s.serverAddress, "Server Address").Layout),
)
}
func init() {
Settings["packs"] = &packsSettings{}
}

View File

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

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

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

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

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

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

@ -0,0 +1,98 @@
package messages
import (
"image"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type MessageResponse struct {
Ok bool
Data interface{}
}
type UIState int
const (
UIStateConnect = iota
UIStateConnecting
UIStateMain
UIStateFinished
)
type HandlerFunc = func(data interface{}) MessageResponse
//
type SetUIState = UIState
//
type SetVoidGen struct {
Value bool
}
//
type SetWorldName struct {
WorldName string
}
//
type Init struct {
Handler HandlerFunc
}
//
type UpdateMap struct {
ChunkCount int
Rotation float32
UpdatedTiles []protocol.ChunkPos
Tiles map[protocol.ChunkPos]*image.RGBA
BoundsMin protocol.ChunkPos
BoundsMax protocol.ChunkPos
}
//
type NewSkin struct {
PlayerName string
Skin *protocol.Skin
}
type SavingWorld struct {
World *SavedWorld
}
type SavedWorld struct {
Name string
Path string
Chunks int
Image image.Image
}
type CanShowImages struct{}
type InitialPacksInfo struct {
Packs []protocol.TexturePackInfo
}
type PackDownloadProgress struct {
UUID string
LoadedAdd uint64
}
type DownloadedPack struct {
UUID string
Name string
Path string
Size int
Icon image.Image
Err error
}
type FinishedDownloadingPacks struct {
Packs []*DownloadedPack
}

1
ui/stub.go Normal file
View File

@ -0,0 +1 @@
package ui

View File

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

View File

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

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

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

View File

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

View File

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

113
utils/chunk_render.go Normal file
View File

@ -0,0 +1,113 @@
package utils
import (
"image"
"image/color"
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
)
func isBlockLightblocking(b world.Block) bool {
d, isDiffuser := b.(block.LightDiffuser)
noDiffuse := isDiffuser && d.LightDiffusionLevel() == 0
return !noDiffuse
}
func blockColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) (blockColor color.RGBA) {
if y <= int16(c.Range().Min()) {
return color.RGBA{0, 0, 0, 0}
}
rid := c.Block(x, y, z, 0)
blockColor = color.RGBA{255, 0, 255, 255}
b, found := world.BlockByRuntimeID(rid)
if !found {
return blockColor
}
if _, isWater := b.(block.Water); isWater {
waterColor := block.Water{}.Color()
// get the first non water block at the position
heightBlock := c.HeightMap().At(x, z)
depth := y - heightBlock
if depth > 0 {
blockColor = blockColorAt(c, x, heightBlock, z)
}
// blend that blocks color with water depending on depth
waterColor.A = uint8(Clamp(int(150+depth*7), 255))
blockColor = BlendColors(blockColor, waterColor)
blockColor.R -= uint8(depth * 2)
blockColor.G -= uint8(depth * 2)
blockColor.B -= uint8(depth * 2)
return blockColor
} else {
col := b.Color()
if col.A != 255 {
col = BlendColors(blockColorAt(c, x, y-1, z), col)
}
/*
a := color.RGBA{255, 0, 255, 255}
if col == a {
name, nbt := b.EncodeBlock()
fmt.Printf("unknown color %d %s %s %s\n", rid, reflect.TypeOf(b), name, nbt)
}
*/
return col
}
}
func chunkGetColorAt(c *chunk.Chunk, x uint8, y int16, z uint8) color.RGBA {
haveUp := false
cube.Pos{int(x), int(y), int(z)}.
Side(cube.FaceUp).
Neighbours(func(neighbour cube.Pos) {
if neighbour.X() < 0 || neighbour.X() >= 16 || neighbour.Z() < 0 || neighbour.Z() >= 16 || neighbour.Y() > c.Range().Max() || haveUp {
return
}
blockRid := c.Block(uint8(neighbour[0]), int16(neighbour[1]), uint8(neighbour[2]), 0)
if blockRid > 0 {
b, found := world.BlockByRuntimeID(blockRid)
if found {
if isBlockLightblocking(b) {
haveUp = true
}
}
}
}, cube.Range{int(y + 1), int(y + 1)})
blockColor := blockColorAt(c, x, y, z)
if haveUp && (x+z)%2 == 0 {
if blockColor.R > 10 {
blockColor.R -= 10
}
if blockColor.G > 10 {
blockColor.G -= 10
}
if blockColor.B > 10 {
blockColor.B -= 10
}
}
return blockColor
}
func Chunk2Img(c *chunk.Chunk) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, 16, 16))
hm := c.HeightMapWithWater()
for x := uint8(0); x < 16; x++ {
for z := uint8(0); z < 16; z++ {
img.SetRGBA(
int(x), int(z),
chunkGetColorAt(c, x, hm.At(x, z), z),
)
}
}
return img
}

View File

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

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

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

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

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

View File

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

129
utils/dumpstruct.go Normal file
View File

@ -0,0 +1,129 @@
package utils
import (
"fmt"
"reflect"
"strings"
"golang.org/x/exp/slices"
)
func DumpStruct(level int, inputStruct any, withType bool, isInList bool) (s string) {
tBase := strings.Repeat("\t", level)
if inputStruct == nil {
return "nil"
}
ii := reflect.Indirect(reflect.ValueOf(inputStruct))
typeName := reflect.TypeOf(inputStruct).String()
if typeName == "[]interface {}" {
typeName = "[]any"
}
typeString := ""
if withType {
if slices.Contains([]string{"bool", "string"}, typeName) {
} else {
typeString = typeName
}
}
if strings.HasPrefix(typeName, "protocol.Optional") {
v := ii.MethodByName("Value").Call(nil)
val, set := v[0], v[1]
if !set.Bool() {
s += typeName + " Not Set"
} else {
s += typeName + "{\n" + tBase + "\t"
s += DumpStruct(level+1, val.Interface(), false, false)
s += "\n" + tBase + "}"
}
return
}
switch ii.Kind() {
case reflect.Struct:
if ii.NumField() == 0 {
s += typeName + "{}"
} else {
s += typeName + "{\n"
for i := 0; i < ii.NumField(); i++ {
fieldType := ii.Type().Field(i)
if fieldType.IsExported() {
s += fmt.Sprintf("%s\t%s: %s,\n", tBase, fieldType.Name, DumpStruct(level+1, ii.Field(i).Interface(), true, false))
} else {
s += tBase + " " + fieldType.Name + " (unexported)"
}
}
s += tBase + "}"
}
case reflect.Slice:
s += typeName + "{"
if ii.Len() > 1000 {
s += "<slice too long>"
} else if ii.Len() == 0 {
} else {
e := ii.Index(0)
t := reflect.TypeOf(e.Interface())
is_elem_struct := t.Kind() == reflect.Struct
if is_elem_struct {
s += "\n"
}
for i := 0; i < ii.Len(); i++ {
if is_elem_struct {
s += tBase + "\t"
}
s += DumpStruct(level+1, ii.Index(i).Interface(), false, true) + ","
if is_elem_struct {
s += "\n"
} else {
if i != ii.Len()-1 {
s += " "
}
}
}
if is_elem_struct {
s += tBase
}
}
s += "}"
case reflect.Map:
it := reflect.TypeOf(inputStruct)
valType := it.Elem().String()
if valType == "interface {}" {
valType = "any"
}
keyType := it.Key().String()
s += fmt.Sprintf("map[%s]%s{", keyType, valType)
if ii.Len() > 0 {
s += "\n"
}
iter := ii.MapRange()
for iter.Next() {
k := iter.Key()
v := iter.Value()
s += fmt.Sprintf("%s\t%#v: %s,\n", tBase, k.Interface(), DumpStruct(level+1, v.Interface(), true, false))
}
if ii.Len() > 0 {
s += tBase
}
s += "}"
default:
is_array := ii.Kind() == reflect.Array
add_type := !isInList && !is_array && len(typeString) > 0
if add_type {
s += typeString + "("
}
s += fmt.Sprintf("%#v", ii.Interface())
if add_type {
s += ")"
}
}
return s
}

219
utils/encryptor/enc.go Normal file
View File

@ -0,0 +1,219 @@
package encryptor
import (
"archive/zip"
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"io"
"io/fs"
"math/rand"
"path/filepath"
"testing/fstest"
"time"
"github.com/google/uuid"
)
type contentItem struct {
Path string `json:"path"`
Key string `json:"key"`
}
type Content struct {
Content []contentItem `json:"content"`
}
var StaticKey = []byte("s5s5ejuDru4uchuF2drUFuthaspAbepE")
func GenerateKey() (out []byte) {
out = make([]byte, 32)
var vocab = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
for i := 0; i < 32; i++ {
out[i] = vocab[rand.Intn(len(vocab))]
}
return
}
func encryptCfb(data []byte, key []byte) {
b, _ := aes.NewCipher(key)
s := cipher.NewCFBEncrypter(b, key[0:16])
s.XORKeyStream(data, data)
}
func canEncrypt(path string) bool {
if path == "manifest.json" {
return false
}
s := filepath.SplitList(path)
if s[0] == "texts" {
return false
}
if s[len(s)-1] == "contents.json" {
return false
}
return true
}
func enc(fsys fs.FS, fsyso fstest.MapFS, contentsJson *Content, dir string) error {
// get all files in this folder
matches, err := fs.Glob(fsys, dir+"**")
if err != nil {
return err
}
for _, path := range matches {
// create output file
ifo, err := fs.Stat(fsys, path)
if err != nil {
return err
}
fo := &fstest.MapFile{
ModTime: ifo.ModTime(),
Mode: ifo.Mode(),
}
fsyso[path] = fo
// recurse
if ifo.IsDir() {
return enc(fsys, fsyso, contentsJson, path+"/")
}
// read data
var data []byte
data, err = fs.ReadFile(fsys, path)
if err != nil {
return err
}
// encrypt if needed
if canEncrypt(path) {
key := GenerateKey()
it := contentItem{
Path: path,
Key: hex.EncodeToString(key),
}
contentsJson.Content = append(contentsJson.Content, it)
encryptCfb(data, key)
}
// write to output
fo.Data = data
}
return nil
}
func Enc(fsys fs.FS, id *uuid.UUID, ContentKey []byte) (fs.FS, error) {
var manifest map[string]any
// read the manifest
f, err := fsys.Open("manifest.json")
if err == nil {
dec := json.NewDecoder(f)
err = dec.Decode(&manifest)
if err != nil {
return nil, err
}
header, ok := manifest["header"].(map[string]any)
if !ok {
return nil, errors.New("no header")
}
// get id from manifest if not specified, else change it in the manifet
if id == nil {
idstr, ok := header["uuid"].(string)
if !ok {
return nil, errors.New("no id")
}
_id, err := uuid.Parse(idstr)
if err != nil {
return nil, err
}
id = &_id
} else {
header["uuid"] = id.String()
}
} else {
if id != nil {
// create a manifest
} else {
return nil, err
}
}
fsyso := fstest.MapFS{}
// encrypt
var contentsJson Content
err = enc(fsys, fsyso, &contentsJson, "")
if err != nil {
return nil, err
}
// write new manifest
manifestData, _ := json.MarshalIndent(manifest, "", "\t")
fsyso["manifest.json"] = &fstest.MapFile{
Data: manifestData,
}
// write the contents.json encrypted
contentsBuf := bytes.NewBuffer(nil)
binary.Write(contentsBuf, binary.LittleEndian, uint32(0))
binary.Write(contentsBuf, binary.LittleEndian, uint32(0x9bcfb9fc))
binary.Write(contentsBuf, binary.LittleEndian, uint64(0))
contentsBuf.WriteByte(byte(len(id.String())))
contentsBuf.Write([]byte(id.String()))
contentsBuf.Write(make([]byte, 0xff-contentsBuf.Len()))
contentsData, _ := json.Marshal(&contentsJson)
encryptCfb(contentsData, ContentKey)
contentsBuf.Write(contentsData)
fsyso["contents.json"] = &fstest.MapFile{
Data: contentsBuf.Bytes(),
Mode: 0775,
ModTime: time.Now(),
}
return fsyso, nil
}
func fstozip(fsys fs.FS, zw *zip.Writer, dir string) error {
// get files in this folder
matches, err := fs.Glob(fsys, dir+"**")
if err != nil {
return err
}
for _, path := range matches {
// if this path is a folder, recurse
ifo, _ := fs.Stat(fsys, path)
if ifo.IsDir() {
return fstozip(fsys, zw, path+"/")
}
// copy the file to the zip
w, err := zw.CreateHeader(&zip.FileHeader{
Name: ifo.Name(),
Modified: ifo.ModTime(),
})
if err != nil {
return err
}
data, err := fs.ReadFile(fsys, path)
if err != nil {
return err
}
w.Write(data)
}
return nil
}
func FSToZip(fsys fs.FS, w io.Writer) error {
zw := zip.NewWriter(w)
err := fstozip(fsys, zw, "")
if err != nil {
return err
}
zw.Close()
return nil
}

View File

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

View File

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

87
utils/iui.go Normal file
View File

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

View File

@ -8,11 +8,11 @@ import (
func InvFromNBT(inv *inventory.Inventory, items []any) {
for _, itemData := range items {
data, _ := itemData.(map[string]any)
it := ReadItem(data, nil)
it := Item(data, nil)
if it.Empty() {
continue
}
_ = inv.SetItem(int(Map[byte](data, "Slot")), it)
_ = inv.SetItem(int(Uint8(data, "Slot")), it)
}
}

View File

@ -1,89 +0,0 @@
package nbtconv
import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
)
// Map reads a value of the type T from the map passed. Map never panics. If the key was not found in the map
// or if the value was of a different type, the default value of type T is returned.
func Map[T any](m map[string]any, key string) T {
v, _ := m[key].(T)
return v
}
// MapVec3 converts x, y and z values in an NBT map to an mgl64.Vec3.
func MapVec3(x map[string]any, k string) mgl64.Vec3 {
if i, ok := x[k].([]any); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
var v mgl64.Vec3
for index, f := range i {
f32, _ := f.(float32)
v[index] = float64(f32)
}
return v
} else if i, ok := x[k].([]float32); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
return mgl64.Vec3{float64(i[0]), float64(i[1]), float64(i[2])}
}
return mgl64.Vec3{}
}
// Vec3ToFloat32Slice converts an mgl64.Vec3 to a []float32 with 3 elements.
func Vec3ToFloat32Slice(x mgl64.Vec3) []float32 {
return []float32{float32(x[0]), float32(x[1]), float32(x[2])}
}
// MapPos converts x, y and z values in an NBT map to a cube.Pos.
func MapPos(x map[string]any, k string) cube.Pos {
if i, ok := x[k].([]any); ok {
if len(i) != 3 {
return cube.Pos{}
}
var v cube.Pos
for index, f := range i {
f32, _ := f.(int32)
v[index] = int(f32)
}
return v
} else if i, ok := x[k].([]int32); ok {
if len(i) != 3 {
return cube.Pos{}
}
return cube.Pos{int(i[0]), int(i[1]), int(i[2])}
}
return cube.Pos{}
}
// PosToInt32Slice converts a cube.Pos to a []int32 with 3 elements.
func PosToInt32Slice(x cube.Pos) []int32 {
return []int32{int32(x[0]), int32(x[1]), int32(x[2])}
}
// MapBlock converts a block's name and properties in a map obtained by decoding NBT to a world.Block.
func MapBlock(x map[string]any, k string) world.Block {
if m, ok := x[k].(map[string]any); ok {
return ReadBlock(m)
}
return nil
}
// MapItem converts an item's name, count, damage (and properties when it is a block) in a map obtained by decoding NBT
// to a world.Item.
func MapItem(x map[string]any, k string) item.Stack {
if m, ok := x[k].(map[string]any); ok {
s := readItemStack(m)
readDamage(m, &s, true)
readEnchantments(m, &s)
readDisplay(m, &s)
readDragonflyData(m, &s)
return s
}
return item.Stack{}
}

View File

@ -3,78 +3,233 @@ package nbtconv
import (
"bytes"
"encoding/gob"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
"golang.org/x/exp/constraints"
"time"
)
// ReadItem decodes the data of an item into an item stack.
func ReadItem(data map[string]any, s *item.Stack) item.Stack {
disk := s == nil
// Bool reads a uint8 value from a map at key k and returns true if it equals 1.
func Bool(m map[string]any, k string) bool {
return Uint8(m, k) == 1
}
// Uint8 reads a uint8 value from a map at key k.
func Uint8(m map[string]any, k string) uint8 {
v, _ := m[k].(uint8)
return v
}
// String reads a string value from a map at key k.
func String(m map[string]any, k string) string {
v, _ := m[k].(string)
return v
}
// Int16 reads an int16 value from a map at key k.
func Int16(m map[string]any, k string) int16 {
v, _ := m[k].(int16)
return v
}
// Int32 reads an int32 value from a map at key k.
func Int32(m map[string]any, k string) int32 {
v, _ := m[k].(int32)
return v
}
// Int64 reads an int16 value from a map at key k.
func Int64(m map[string]any, k string) int64 {
v, _ := m[k].(int64)
return v
}
// TickDuration reads a uint8/int16/in32 value from a map at key k and converts
// it from ticks to a time.Duration.
func TickDuration[T constraints.Integer](m map[string]any, k string) time.Duration {
var v time.Duration
switch any(*new(T)).(type) {
case uint8:
v = time.Duration(Uint8(m, k))
case int16:
v = time.Duration(Int16(m, k))
case int32:
v = time.Duration(Int32(m, k))
default:
panic("invalid tick duration value type")
}
return v * time.Millisecond * 50
}
// Float32 reads a float32 value from a map at key k.
func Float32(m map[string]any, k string) float32 {
v, _ := m[k].(float32)
return v
}
// Float64 reads a float64 value from a map at key k.
func Float64(m map[string]any, k string) float64 {
v, _ := m[k].(float64)
return v
}
// Slice reads a []any value from a map at key k.
func Slice(m map[string]any, k string) []any {
v, _ := m[k].([]any)
return v
}
// Vec3 converts x, y and z values in an NBT map to an mgl64.Vec3.
func Vec3(x map[string]any, k string) mgl64.Vec3 {
if i, ok := x[k].([]any); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
var v mgl64.Vec3
for index, f := range i {
f32, _ := f.(float32)
v[index] = float64(f32)
}
return v
} else if i, ok := x[k].([]float32); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
return mgl64.Vec3{float64(i[0]), float64(i[1]), float64(i[2])}
}
return mgl64.Vec3{}
}
// Vec3ToFloat32Slice converts an mgl64.Vec3 to a []float32 with 3 elements.
func Vec3ToFloat32Slice(x mgl64.Vec3) []float32 {
return []float32{float32(x[0]), float32(x[1]), float32(x[2])}
}
// Pos converts x, y and z values in an NBT map to a cube.Pos.
func Pos(x map[string]any, k string) cube.Pos {
if i, ok := x[k].([]any); ok {
if len(i) != 3 {
return cube.Pos{}
}
var v cube.Pos
for index, f := range i {
f32, _ := f.(int32)
v[index] = int(f32)
}
return v
} else if i, ok := x[k].([]int32); ok {
if len(i) != 3 {
return cube.Pos{}
}
return cube.Pos{int(i[0]), int(i[1]), int(i[2])}
}
return cube.Pos{}
}
// PosToInt32Slice converts a cube.Pos to a []int32 with 3 elements.
func PosToInt32Slice(x cube.Pos) []int32 {
return []int32{int32(x[0]), int32(x[1]), int32(x[2])}
}
// MapItem converts an item's name, count, damage (and properties when it is a block) in a map obtained by decoding NBT
// to a world.Item.
func MapItem(x map[string]any, k string) item.Stack {
if m, ok := x[k].(map[string]any); ok {
tag, ok := m["tag"].(map[string]any)
if !ok {
tag = map[string]any{}
}
s := readItemStack(m, tag)
readDamage(tag, &s, true)
readEnchantments(tag, &s)
readDisplay(tag, &s)
readDragonflyData(tag, &s)
return s
}
return item.Stack{}
}
// Item decodes the data of an item into an item stack.
func Item(data map[string]any, s *item.Stack) item.Stack {
disk, tag := s == nil, data
if disk {
a := readItemStack(data)
t, ok := data["tag"].(map[string]any)
if !ok {
t = map[string]any{}
}
tag = t
a := readItemStack(data, tag)
s = &a
}
readDamage(data, s, disk)
readAnvilCost(data, s)
readDisplay(data, s)
readEnchantments(data, s)
readDragonflyData(data, s)
readAnvilCost(tag, s)
readDamage(tag, s, disk)
readDisplay(tag, s)
readDragonflyData(tag, s)
readEnchantments(tag, s)
return *s
}
// ReadBlock decodes the data of a block into a world.Block.
func ReadBlock(m map[string]any) world.Block {
name, _ := m["name"].(string)
properties, _ := m["states"].(map[string]any)
b, _ := world.BlockByName(name, properties)
return b
// Block decodes the data of a block into a world.Block.
func Block(m map[string]any, k string) world.Block {
if mk, ok := m[k].(map[string]any); ok {
name, _ := mk["name"].(string)
properties, _ := mk["states"].(map[string]any)
b, _ := world.BlockByName(name, properties)
return b
}
return nil
}
// readItemStack reads an item.Stack from the NBT in the map passed.
func readItemStack(m map[string]any) item.Stack {
func readItemStack(m, t map[string]any) item.Stack {
var it world.Item
if blockItem, ok := MapBlock(m, "Block").(world.Item); ok {
if blockItem, ok := Block(m, "Block").(world.Item); ok {
it = blockItem
}
if v, ok := world.ItemByName(Map[string](m, "Name"), Map[int16](m, "Damage")); ok {
if v, ok := world.ItemByName(String(m, "Name"), Int16(m, "Damage")); ok {
it = v
}
if it == nil {
return item.Stack{}
}
if n, ok := it.(world.NBTer); ok {
it = n.DecodeNBT(m).(world.Item)
it = n.DecodeNBT(t).(world.Item)
}
return item.NewStack(it, int(Map[byte](m, "Count")))
return item.NewStack(it, int(Uint8(m, "Count")))
}
// readDamage reads the damage value stored in the NBT with the Damage tag and saves it to the item.Stack passed.
func readDamage(m map[string]any, s *item.Stack, disk bool) {
if disk {
*s = s.Damage(int(Map[int16](m, "Damage")))
*s = s.Damage(int(Int16(m, "Damage")))
return
}
*s = s.Damage(int(Map[int32](m, "Damage")))
*s = s.Damage(int(Int32(m, "Damage")))
}
// readAnvilCost ...
func readAnvilCost(m map[string]any, s *item.Stack) {
*s = s.WithAnvilCost(int(Map[int32](m, "RepairCost")))
*s = s.WithAnvilCost(int(Int32(m, "RepairCost")))
}
// readEnchantments reads the enchantments stored in the ench tag of the NBT passed and stores it into an item.Stack.
func readEnchantments(m map[string]any, s *item.Stack) {
enchantments, ok := m["ench"].([]map[string]any)
if !ok {
for _, e := range Map[[]any](m, "ench") {
for _, e := range Slice(m, "ench") {
if v, ok := e.(map[string]any); ok {
enchantments = append(enchantments, v)
}
}
}
for _, ench := range enchantments {
if t, ok := item.EnchantmentByID(int(Map[int16](ench, "id"))); ok {
*s = s.WithEnchantments(item.NewEnchantment(t, int(Map[int16](ench, "lvl"))))
if t, ok := item.EnchantmentByID(int(Int16(ench, "id"))); ok {
*s = s.WithEnchantments(item.NewEnchantment(t, int(Int16(ench, "lvl"))))
}
}
}

View File

@ -11,21 +11,27 @@ import (
// WriteItem encodes an item stack into a map that can be encoded using NBT.
func WriteItem(s item.Stack, disk bool) map[string]any {
m := make(map[string]any)
tag := make(map[string]any)
if nbt, ok := s.Item().(world.NBTer); ok {
for k, v := range nbt.EncodeNBT() {
m[k] = v
tag[k] = v
}
}
writeAnvilCost(tag, s)
writeDamage(tag, s, disk)
writeDisplay(tag, s)
writeDragonflyData(tag, s)
writeEnchantments(tag, s)
data := make(map[string]any)
if disk {
writeItemStack(m, s)
writeItemStack(data, tag, s)
} else {
for k, v := range tag {
data[k] = v
}
}
writeDamage(m, s, disk)
writeAnvilCost(m, s)
writeDisplay(m, s)
writeEnchantments(m, s)
writeDragonflyData(m, s)
return m
return data
}
// WriteBlock encodes a world.Block into a map that can be encoded using NBT.
@ -39,7 +45,7 @@ func WriteBlock(b world.Block) map[string]any {
}
// writeItemStack writes the name, metadata value, count and NBT of an item to a map ready for NBT encoding.
func writeItemStack(m map[string]any, s item.Stack) {
func writeItemStack(m, t map[string]any, s item.Stack) {
m["Name"], m["Damage"] = s.Item().EncodeItem()
if b, ok := s.Item().(world.Block); ok {
v := map[string]any{}
@ -47,6 +53,9 @@ func writeItemStack(m map[string]any, s item.Stack) {
m["Block"] = v
}
m["Count"] = byte(s.Count())
if len(t) > 0 {
m["tag"] = t
}
}
// writeBlock writes the name, properties and version of a block to a map ready for NBT encoding.

View File

@ -1,8 +1,12 @@
package utils
import "net"
import (
"net"
var PrivateIPNetworks = []net.IPNet{
"github.com/repeale/fp-go"
)
var privateIPNetworks = []net.IPNet{
{
IP: net.ParseIP("10.0.0.0"),
Mask: net.CIDRMask(8, 32),
@ -17,14 +21,11 @@ var PrivateIPNetworks = []net.IPNet{
},
}
// check if ip is private
// IPPrivate checks if ip is private
func IPPrivate(ip net.IP) bool {
for _, ipNet := range PrivateIPNetworks {
if ipNet.Contains(ip) {
return true
}
}
return false
return fp.Some(func(ipNet net.IPNet) bool {
return ipNet.Contains(ip)
})(privateIPNetworks)
}
// GetLocalIP returns the non loopback local IP of the host

View File

@ -1,93 +0,0 @@
package utils
import (
"bytes"
"net"
"reflect"
"github.com/fatih/color"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
var Pool = packet.NewPool()
var MutedPackets = []string{
"packet.UpdateBlock",
"packet.MoveActorAbsolute",
"packet.SetActorMotion",
"packet.SetTime",
"packet.RemoveActor",
"packet.AddActor",
"packet.UpdateAttributes",
"packet.Interact",
"packet.LevelEvent",
"packet.SetActorData",
"packet.MoveActorDelta",
"packet.MovePlayer",
"packet.BlockActorData",
"packet.PlayerAuthInput",
"packet.LevelChunk",
"packet.LevelSoundEvent",
"packet.ActorEvent",
"packet.NetworkChunkPublisherUpdate",
"packet.UpdateSubChunkBlocks",
"packet.SubChunk",
"packet.SubChunkRequest",
"packet.Animate",
"packet.NetworkStackLatency",
"packet.InventoryTransaction",
"packet.PlaySound",
}
var ExtraVerbose []string
func PacketLogger(header packet.Header, payload []byte, src, dst net.Addr) {
var pk packet.Packet
if pkFunc, ok := Pool[header.PacketID]; ok {
pk = pkFunc()
} else {
pk = &packet.Unknown{PacketID: header.PacketID}
}
defer func() {
if recoveredErr := recover(); recoveredErr != nil {
logrus.Errorf("%T: %w", pk, recoveredErr.(error))
}
}()
pk.Unmarshal(protocol.NewReader(bytes.NewBuffer(payload), 0))
pk_name := reflect.TypeOf(pk).String()[1:]
if slices.Contains(MutedPackets, pk_name) {
return
}
switch pk := pk.(type) {
case *packet.Disconnect:
logrus.Infof("Disconnect: %s", pk.Message)
}
dir_S2C := color.GreenString("S") + "->" + color.CyanString("C")
dir_C2S := color.CyanString("C") + "->" + color.GreenString("S")
var dir string = dir_S2C
if Client_addr != nil {
if src == Client_addr {
dir = dir_C2S
}
} else {
src_addr, _, _ := net.SplitHostPort(src.String())
if IPPrivate(net.ParseIP(src_addr)) {
dir = dir_C2S
}
}
logrus.Debugf("%s 0x%02x, %s", dir, pk.ID(), pk_name)
if slices.Contains(ExtraVerbose, pk_name) {
logrus.Debugf("%+v", pk)
}
}

View File

@ -1,22 +1,32 @@
package utils
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"reflect"
"strings"
"sync"
"time"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/repeale/fp-go"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
)
var G_disconnect_reason = "Connection lost"
var DisconnectReason = "Connection lost"
/*
type dummyProto struct {
id int32
ver string
@ -32,60 +42,180 @@ func (p dummyProto) ConvertToLatest(pk packet.Packet, _ *minecraft.Conn) []packe
func (p dummyProto) ConvertFromLatest(pk packet.Packet, _ *minecraft.Conn) []packet.Packet {
return []packet.Packet{pk}
}
type ProxyContext struct {
Server *minecraft.Conn
Client *minecraft.Conn
Listener *minecraft.Listener
commands map[string]IngameCommand
log *logrus.Logger
// called for every packet
PacketFunc PacketFunc
// called after game started
ConnectCB ConnectCallback
// called on every packet after login
PacketCB PacketCallback
}
*/
type (
PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr)
PacketCallback func(pk packet.Packet, proxy *ProxyContext, toServer bool) (packet.Packet, error)
ConnectCallback func(proxy *ProxyContext)
PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr)
IngameCommand struct {
Exec func(cmdline []string) bool
Cmd protocol.Command
}
)
func (p *ProxyContext) SendMessage(text string) {
if p.Client != nil {
p.Client.WritePacket(&packet.Text{
TextType: packet.TextTypeSystem,
Message: "§8[§bBedrocktool§8]§r " + text,
})
}
type ProxyHandler struct {
Name string
ProxyRef func(pc *ProxyContext)
//
AddressAndName func(address, hostname string) error
// called to change game data
GameDataModifier func(gd *minecraft.GameData)
// called for every packet
PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr)
// called on every packet after login
PacketCB func(pk packet.Packet, toServer bool, timeReceived time.Time) (packet.Packet, error)
// called after client connected
OnClientConnect func(conn minecraft.IConn)
SecondaryClientCB func(conn minecraft.IConn)
// called after server connected & downloaded resource packs
OnServerConnect func() (cancel bool)
// called after game started
ConnectCB func(err error) bool
// called when the proxy stops
OnEnd func()
}
func (p *ProxyContext) SendPopup(text string) {
if p.Client != nil {
p.Client.WritePacket(&packet.Text{
TextType: packet.TextTypePopup,
Message: text,
})
}
type ProxyContext struct {
Server minecraft.IConn
Client minecraft.IConn
clientAddr net.Addr
Listener *minecraft.Listener
AlwaysGetPacks bool
WithClient bool
IgnoreDisconnect bool
CustomClientData *login.ClientData
commands map[string]IngameCommand
handlers []*ProxyHandler
}
type IngameCommand struct {
Exec func(cmdline []string) bool
Cmd protocol.Command
func NewProxy() (*ProxyContext, error) {
p := &ProxyContext{
commands: make(map[string]IngameCommand),
AlwaysGetPacks: false,
WithClient: true,
IgnoreDisconnect: false,
}
if Options.PathCustomUserData != "" {
if err := p.LoadCustomUserData(Options.PathCustomUserData); err != nil {
return nil, err
}
}
return p, nil
}
func (p *ProxyContext) AddCommand(cmd IngameCommand) {
p.commands[cmd.Cmd.Name] = cmd
}
func (p *ProxyContext) CommandHandlerPacketCB(pk packet.Packet, proxy *ProxyContext, toServer bool) (packet.Packet, error) {
switch _pk := pk.(type) {
type CustomClientData struct {
// skin things
CapeFilename string
SkinFilename string
SkinGeometryFilename string
SkinID string
PlayFabID string
PersonaSkin bool
PremiumSkin bool
TrustedSkin bool
ArmSize string
SkinColour string
// misc
IsEditorMode bool
LanguageCode string
DeviceID string
}
func (p *ProxyContext) LoadCustomUserData(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
var customData CustomClientData
err = json.NewDecoder(f).Decode(&customData)
if err != nil {
return err
}
p.CustomClientData = &login.ClientData{
SkinID: customData.SkinID,
PlayFabID: customData.PlayFabID,
PersonaSkin: customData.PersonaSkin,
PremiumSkin: customData.PremiumSkin,
TrustedSkin: customData.TrustedSkin,
ArmSize: customData.ArmSize,
SkinColour: customData.SkinColour,
}
if customData.SkinFilename != "" {
img, err := loadPng(customData.SkinFilename)
if err != nil {
return err
}
p.CustomClientData.SkinData = base64.RawStdEncoding.EncodeToString(img.Pix)
p.CustomClientData.SkinImageWidth = img.Rect.Dx()
p.CustomClientData.SkinImageHeight = img.Rect.Dy()
}
if customData.CapeFilename != "" {
img, err := loadPng(customData.CapeFilename)
if err != nil {
return err
}
p.CustomClientData.CapeData = base64.RawStdEncoding.EncodeToString(img.Pix)
p.CustomClientData.CapeImageWidth = img.Rect.Dx()
p.CustomClientData.CapeImageHeight = img.Rect.Dy()
}
if customData.SkinGeometryFilename != "" {
data, err := os.ReadFile(customData.SkinGeometryFilename)
if err != nil {
return err
}
p.CustomClientData.SkinGeometry = base64.RawStdEncoding.EncodeToString(data)
}
p.CustomClientData.DeviceID = customData.DeviceID
return nil
}
func (p *ProxyContext) ClientWritePacket(pk packet.Packet) error {
if p.Client == nil {
return nil
}
return p.Client.WritePacket(pk)
}
func (p *ProxyContext) SendMessage(text string) {
p.ClientWritePacket(&packet.Text{
TextType: packet.TextTypeSystem,
Message: "§8[§bBedrocktool§8]§r " + text,
})
}
func (p *ProxyContext) SendPopup(text string) {
p.ClientWritePacket(&packet.Text{
TextType: packet.TextTypePopup,
Message: text,
})
}
func (p *ProxyContext) AddHandler(handler *ProxyHandler) {
p.handlers = append(p.handlers, handler)
}
func (p *ProxyContext) CommandHandlerPacketCB(pk packet.Packet, toServer bool, _ time.Time) (packet.Packet, error) {
switch pk := pk.(type) {
case *packet.CommandRequest:
cmd := strings.Split(_pk.CommandLine, " ")
cmd := strings.Split(pk.CommandLine, " ")
name := cmd[0][1:]
if h, ok := p.commands[name]; ok {
if h.Exec(cmd[1:]) {
@ -93,26 +223,26 @@ func (p *ProxyContext) CommandHandlerPacketCB(pk packet.Packet, proxy *ProxyCont
}
}
case *packet.AvailableCommands:
cmds := make([]protocol.Command, len(p.commands))
cmds := make([]protocol.Command, 0, len(p.commands))
for _, ic := range p.commands {
cmds = append(cmds, ic.Cmd)
}
pk = &packet.AvailableCommands{
Constraints: _pk.Constraints,
Commands: append(_pk.Commands, cmds...),
Constraints: pk.Constraints,
Commands: append(pk.Commands, cmds...),
}
}
return pk, nil
}
func proxyLoop(ctx context.Context, proxy *ProxyContext, toServer bool, packetCBs []PacketCallback) error {
var c1, c2 *minecraft.Conn
func (p *ProxyContext) proxyLoop(ctx context.Context, toServer bool) error {
var c1, c2 minecraft.IConn
if toServer {
c1 = proxy.Client
c2 = proxy.Server
c1 = p.Client
c2 = p.Server
} else {
c1 = proxy.Server
c2 = proxy.Client
c1 = p.Server
c2 = p.Client
}
for {
@ -125,17 +255,24 @@ func proxyLoop(ctx context.Context, proxy *ProxyContext, toServer bool, packetCB
return err
}
for _, packetCB := range packetCBs {
pk, err = packetCB(pk, proxy, toServer)
if err != nil {
return err
pkName := reflect.TypeOf(pk).String()
for _, handler := range p.handlers {
if handler.PacketCB != nil {
pk, err = handler.PacketCB(pk, toServer, time.Now())
if err != nil {
return err
}
if pk == nil {
logrus.Tracef("Dropped Packet: %s", pkName)
break
}
}
}
if pk != nil {
if pk != nil && c2 != nil {
if err := c2.WritePacket(pk); err != nil {
if disconnect, ok := errors.Unwrap(err).(minecraft.DisconnectError); ok {
G_disconnect_reason = disconnect.Error()
DisconnectReason = disconnect.Error()
}
return err
}
@ -143,111 +280,276 @@ func proxyLoop(ctx context.Context, proxy *ProxyContext, toServer bool, packetCB
}
}
func NewProxy(log *logrus.Logger) *ProxyContext {
if log == nil {
log = logrus.StandardLogger()
}
return &ProxyContext{
log: log,
commands: make(map[string]IngameCommand),
}
// Disconnect disconnects both the client and server
func (p *ProxyContext) Disconnect() {
p.DisconnectClient()
p.DisconnectServer()
}
var Client_addr net.Addr
func (p *ProxyContext) Run(ctx context.Context, server_address string) (err error) {
if strings.HasSuffix(server_address, ".pcap") {
return fmt.Errorf("not supported anymore")
}
if strings.HasSuffix(server_address, ".pcap2") {
return create_replay_connection(ctx, p.log, server_address, p.ConnectCB, p.PacketCB)
// Disconnect disconnects the client
func (p *ProxyContext) DisconnectClient() {
if p.Client == nil {
return
}
p.Client.Close()
}
// Disconnect disconnects from the server
func (p *ProxyContext) DisconnectServer() {
if p.Server == nil {
return
}
p.Server.Close()
}
func (p *ProxyContext) IsClient(addr net.Addr) bool {
return p.clientAddr.String() == addr.String()
}
var NewDebugLogger func(bool) *ProxyHandler
var NewPacketCapturer func() *ProxyHandler
func (p *ProxyContext) connectClient(ctx context.Context, serverAddress string, cdpp **login.ClientData) (err error) {
GetTokenSource() // ask for login before listening
var packs []*resource.Pack
if G_preload_packs {
p.log.Info("Preloading resourcepacks")
var serverConn *minecraft.Conn
serverConn, err = ConnectServer(ctx, server_address, nil, true, nil)
if Options.Preload {
logrus.Info(locale.Loc("preloading_packs", nil))
serverConn, err := connectServer(ctx, serverAddress, nil, true, func(header packet.Header, payload []byte, src, dst net.Addr) {})
if err != nil {
err = fmt.Errorf("failed to connect to %s: %s", server_address, err)
return
return fmt.Errorf(locale.Loc("failed_to_connect", locale.Strmap{"Address": serverAddress, "Err": err}))
}
serverConn.Close()
packs = serverConn.ResourcePacks()
p.log.Infof("%d packs loaded", len(packs))
logrus.Infof(locale.Locm("pack_count_loaded", locale.Strmap{"Count": len(packs)}, len(packs)))
}
_status := minecraft.NewStatusProvider("Server")
status := minecraft.NewStatusProvider(fmt.Sprintf("%s Proxy", serverAddress))
p.Listener, err = minecraft.ListenConfig{
StatusProvider: _status,
ResourcePacks: packs,
StatusProvider: status,
ResourcePacks: packs,
AcceptedProtocols: []minecraft.Protocol{
dummyProto{id: 544, ver: "1.19.20"},
//dummyProto{id: 567, ver: "1.19.60"},
},
}.Listen("raknet", ":19132")
if err != nil {
return
return err
}
defer p.Listener.Close()
p.log.Infof("Listening on %s", p.Listener.Addr())
p.log.Infof("Open Minecraft and connect to this computers local ip address to continue")
logrus.Infof(locale.Loc("listening_on", locale.Strmap{"Address": p.Listener.Addr()}))
logrus.Infof(locale.Loc("help_connect", nil))
var c net.Conn
c, err = p.Listener.Accept()
go func() {
<-ctx.Done()
p.Listener.Close()
}()
c, err := p.Listener.Accept()
if err != nil {
p.log.Fatal(err)
return err
}
p.Client = c.(*minecraft.Conn)
cd := p.Client.ClientData()
p.Server, err = ConnectServer(ctx, server_address, &cd, false, p.PacketFunc)
*cdpp = &cd
return nil
}
func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err error) {
if Options.Debug || Options.ExtraDebug {
p.AddHandler(NewDebugLogger(Options.ExtraDebug))
}
if Options.Capture {
p.AddHandler(NewPacketCapturer())
}
p.AddHandler(&ProxyHandler{
Name: "Commands",
PacketCB: p.CommandHandlerPacketCB,
})
for _, handler := range p.handlers {
if handler.AddressAndName != nil {
handler.AddressAndName(serverAddress, name)
}
if handler.ProxyRef != nil {
handler.ProxyRef(p)
}
}
defer func() {
for _, handler := range p.handlers {
if handler.OnEnd != nil {
handler.OnEnd()
}
}
}()
isReplay := false
if strings.HasPrefix(serverAddress, "PCAP!") {
isReplay = true
}
var cdp *login.ClientData = nil
if p.WithClient && !isReplay {
err = p.connectClient(ctx, serverAddress, &cdp)
if err != nil {
return err
}
defer func() {
if p.Listener != nil {
if p.Client != nil {
p.Listener.Disconnect(p.Client.(*minecraft.Conn), DisconnectReason)
}
p.Listener.Close()
}
}()
}
if p.CustomClientData != nil {
cdp = p.CustomClientData
}
for _, handler := range p.handlers {
if handler.OnClientConnect == nil {
continue
}
handler.OnClientConnect(p.Client)
}
packetFunc := func(header packet.Header, payload []byte, src, dst net.Addr) {
if header.PacketID == packet.IDRequestNetworkSettings {
p.clientAddr = src
}
for _, handler := range p.handlers {
if handler.PacketFunc == nil {
continue
}
handler.PacketFunc(header, payload, src, dst)
}
}
if isReplay {
p.Server, err = createReplayConnector(serverAddress[5:], packetFunc)
if err != nil {
return err
}
} else {
p.Server, err = connectServer(ctx, serverAddress, cdp, p.AlwaysGetPacks, packetFunc)
}
if err != nil {
err = fmt.Errorf("failed to connect to %s: %s", server_address, err)
return
}
// spawn and start the game
if err = spawn_conn(ctx, p.Client, p.Server); err != nil {
err = fmt.Errorf("failed to spawn: %s", err)
return
}
for _, handler := range p.handlers {
if handler.ConnectCB == nil {
continue
}
ignore := handler.ConnectCB(err)
if ignore {
err = nil
break
}
}
if err != nil {
err = fmt.Errorf(locale.Loc("failed_to_connect", locale.Strmap{"Address": serverAddress, "Err": err}))
}
return err
}
defer p.Server.Close()
defer p.Listener.Disconnect(p.Client, G_disconnect_reason)
if p.ConnectCB != nil {
p.ConnectCB(p)
for _, handler := range p.handlers {
if handler.OnServerConnect == nil {
continue
}
cancel := handler.OnServerConnect()
if cancel {
return nil
}
}
gd := p.Server.GameData()
for _, handler := range p.handlers {
if handler.GameDataModifier != nil {
handler.GameDataModifier(&gd)
}
}
// spawn and start the game
if err = spawnConn(ctx, p.Client, p.Server, gd); err != nil {
err = fmt.Errorf(locale.Loc("failed_to_spawn", locale.Strmap{"Err": err}))
return err
}
for _, handler := range p.handlers {
if handler.ConnectCB == nil {
continue
}
if !handler.ConnectCB(nil) {
logrus.Info("Disconnecting")
return nil
}
}
wg := sync.WaitGroup{}
cbs := []PacketCallback{
p.CommandHandlerPacketCB,
}
if p.PacketCB != nil {
cbs = append(cbs, p.PacketCB)
doProxy := func(client bool, onErr func()) {
defer wg.Done()
if err := p.proxyLoop(ctx, client); err != nil {
logrus.Error(err)
return
}
}
// server to client
wg.Add(1)
go func() {
defer wg.Done()
if err := proxyLoop(ctx, p, false, cbs); err != nil {
p.log.Error(err)
return
}
}()
go doProxy(false, func() {
p.DisconnectClient()
})
// client to server
wg.Add(1)
go func() {
defer wg.Done()
if err := proxyLoop(ctx, p, true, cbs); err != nil {
p.log.Error(err)
return
}
}()
if p.Client != nil {
wg.Add(1)
go doProxy(true, func() {
p.DisconnectServer()
})
}
wantSecondary := fp.Filter(func(handler *ProxyHandler) bool {
return handler.SecondaryClientCB != nil
})(p.handlers)
if len(wantSecondary) > 0 {
go func() {
for {
c, err := p.Listener.Accept()
if err != nil {
logrus.Error(err)
return
}
for _, handler := range wantSecondary {
go handler.SecondaryClientCB(c.(*minecraft.Conn))
}
}
}()
}
wg.Wait()
return err
}
var pool = packet.NewPool()
func DecodePacket(header packet.Header, payload []byte) packet.Packet {
var pk packet.Packet
if pkFunc, ok := pool[header.PacketID]; ok {
pk = pkFunc()
} else {
pk = &packet.Unknown{PacketID: header.PacketID, Payload: payload}
}
defer func() {
if recoveredErr := recover(); recoveredErr != nil {
logrus.Errorf("%T: %s", pk, recoveredErr.(error))
}
}()
pk.Marshal(protocol.NewReader(bytes.NewBuffer(payload), 0))
return pk
}

View File

@ -2,22 +2,17 @@ package utils
import (
"context"
"flag"
"fmt"
"strings"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft/realms"
"github.com/sirupsen/logrus"
)
func get_realm(ctx context.Context, api *realms.Client, realm_name, id string) (name string, address string, err error) {
realms, err := api.Realms(ctx)
func getRealm(ctx context.Context, realmName, id string) (name string, address string, err error) {
realms, err := GetRealmsAPI().Realms(ctx)
if err != nil {
return "", "", err
}
for _, realm := range realms {
if strings.HasPrefix(realm.Name, realm_name) {
if strings.HasPrefix(realm.Name, realmName) {
if id != "" && id != fmt.Sprint(id) {
continue
}
@ -31,30 +26,3 @@ func get_realm(ctx context.Context, api *realms.Client, realm_name, id string) (
}
return "", "", fmt.Errorf("realm not found")
}
type RealmListCMD struct{}
func (*RealmListCMD) Name() string { return "list-realms" }
func (*RealmListCMD) Synopsis() string { return "prints all realms you have access to" }
func (c *RealmListCMD) SetFlags(f *flag.FlagSet) {}
func (c *RealmListCMD) Usage() string {
return c.Name() + ": " + c.Synopsis() + "\n"
}
func (c *RealmListCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
api := realms.NewClient(GetTokenSource())
realms, err := api.Realms(ctx)
if err != nil {
logrus.Error(err)
return 1
}
for _, realm := range realms {
fmt.Printf("Name: %s\tid: %d\n", realm.Name, realm.ID)
}
return 0
}
func init() {
RegisterCommand(&RealmListCMD{})
}

View File

@ -1,128 +1,466 @@
package utils
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"os"
"reflect"
"sync"
"time"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
)
func create_replay_connection(ctx context.Context, log *logrus.Logger, filename string, onConnect ConnectCallback, packetCB PacketCallback) error {
log.Infof("Reading replay %s", filename)
type replayHeader struct {
Version int32
}
f, err := os.Open(filename)
var replayMagic = []byte("BTCP")
const (
currentReplayVersion = 2
)
func WriteReplayHeader(f io.Writer) {
f.Write(replayMagic)
header := replayHeader{
Version: currentReplayVersion,
}
binary.Write(f, binary.LittleEndian, &header)
}
type replayConnector struct {
f *os.File
totalSize int64
ver int
packets chan packet.Packet
spawn chan struct{}
close chan struct{}
once sync.Once
pool packet.Pool
proto minecraft.Protocol
clientData login.ClientData
gameData minecraft.GameData
packetFunc PacketFunc
downloadingPacks map[string]*downloadingPack
resourcePacks []*resource.Pack
}
// downloadingPack is a resource pack that is being downloaded by a client connection.
type downloadingPack struct {
buf *bytes.Buffer
chunkSize uint32
size uint64
expectedIndex uint32
newFrag chan []byte
contentKey string
}
func (r *replayConnector) readHeader() error {
r.ver = 1
magic := make([]byte, 4)
io.ReadAtLeast(r.f, magic, 4)
if bytes.Equal(magic, replayMagic) {
var header replayHeader
if err := binary.Read(r.f, binary.LittleEndian, &header); err != nil {
return err
}
r.ver = int(header.Version)
} else {
logrus.Info("Version 1 capture assumed.")
r.f.Seek(-4, io.SeekCurrent)
}
return nil
}
func (r *replayConnector) readPacket() (payload []byte, toServer bool, err error) {
var magic uint32 = 0
var packetLength uint32 = 0
timeReceived := time.Now()
offset, _ := r.f.Seek(0, io.SeekCurrent)
if offset == r.totalSize {
logrus.Info("Reached End")
return nil, toServer, nil
}
binary.Read(r.f, binary.LittleEndian, &magic)
if magic != 0xAAAAAAAA {
return nil, toServer, fmt.Errorf("wrong Magic")
}
binary.Read(r.f, binary.LittleEndian, &packetLength)
binary.Read(r.f, binary.LittleEndian, &toServer)
if r.ver >= 2 {
var timeMs int64
binary.Read(r.f, binary.LittleEndian, &timeMs)
timeReceived = time.UnixMilli(timeMs)
}
payload = make([]byte, packetLength)
n, err := r.f.Read(payload)
if err != nil {
return err
return nil, toServer, err
}
var size int64
{
stat, err := f.Stat()
if err != nil {
return err
if n != int(packetLength) {
return nil, toServer, fmt.Errorf("truncated")
}
var magic2 uint32
binary.Read(r.f, binary.LittleEndian, &magic2)
if magic2 != 0xBBBBBBBB {
return nil, toServer, fmt.Errorf("wrong Magic2")
}
_ = timeReceived
return payload, toServer, nil
}
func (r *replayConnector) handleLoginSequence(pk packet.Packet) (bool, error) {
switch pk := pk.(type) {
case *packet.StartGame:
r.SetGameData(minecraft.GameData{
WorldName: pk.WorldName,
WorldSeed: pk.WorldSeed,
Difficulty: pk.Difficulty,
EntityUniqueID: pk.EntityUniqueID,
EntityRuntimeID: pk.EntityRuntimeID,
PlayerGameMode: pk.PlayerGameMode,
PersonaDisabled: pk.PersonaDisabled,
CustomSkinsDisabled: pk.CustomSkinsDisabled,
BaseGameVersion: pk.BaseGameVersion,
PlayerPosition: pk.PlayerPosition,
Pitch: pk.Pitch,
Yaw: pk.Yaw,
Dimension: pk.Dimension,
WorldSpawn: pk.WorldSpawn,
EditorWorld: pk.EditorWorld,
WorldGameMode: pk.WorldGameMode,
GameRules: pk.GameRules,
Time: pk.Time,
ServerBlockStateChecksum: pk.ServerBlockStateChecksum,
CustomBlocks: pk.Blocks,
Items: pk.Items,
PlayerMovementSettings: pk.PlayerMovementSettings,
ServerAuthoritativeInventory: pk.ServerAuthoritativeInventory,
Experiments: pk.Experiments,
ClientSideGeneration: pk.ClientSideGeneration,
ChatRestrictionLevel: pk.ChatRestrictionLevel,
DisablePlayerInteractions: pk.DisablePlayerInteractions,
})
case *packet.ResourcePacksInfo:
for _, pack := range pk.TexturePacks {
r.downloadingPacks[pack.UUID] = &downloadingPack{
size: pack.Size,
buf: bytes.NewBuffer(make([]byte, 0, pack.Size)),
newFrag: make(chan []byte),
contentKey: pack.ContentKey,
}
}
size = stat.Size()
case *packet.ResourcePackDataInfo:
pack, ok := r.downloadingPacks[pk.UUID]
if !ok {
// We either already downloaded the pack or we got sent an invalid UUID, that did not match any pack
// sent in the ResourcePacksInfo packet.
return false, fmt.Errorf("unknown pack to download with UUID %v", pk.UUID)
}
if pack.size != pk.Size {
// Size mismatch: The ResourcePacksInfo packet had a size for the pack that did not match with the
// size sent here.
logrus.Printf("pack %v had a different size in the ResourcePacksInfo packet than the ResourcePackDataInfo packet\n", pk.UUID)
pack.size = pk.Size
}
pack.chunkSize = pk.DataChunkSize
chunkCount := uint32(pk.Size / uint64(pk.DataChunkSize))
if pk.Size%uint64(pk.DataChunkSize) != 0 {
chunkCount++
}
go func() {
for i := uint32(0); i < chunkCount; i++ {
select {
case <-r.close:
return
case frag := <-pack.newFrag:
// Write the fragment to the full buffer of the downloading resource pack.
_, _ = pack.buf.Write(frag)
}
}
if pack.buf.Len() != int(pack.size) {
logrus.Printf("incorrect resource pack size: expected %v, but got %v\n", pack.size, pack.buf.Len())
return
}
// First parse the resource pack from the total byte buffer we obtained.
newPack, err := resource.FromBytes(pack.buf.Bytes())
if err != nil {
logrus.Printf("invalid full resource pack data for UUID %v: %v\n", pk.UUID, err)
return
}
r.resourcePacks = append(r.resourcePacks, newPack.WithContentKey(pack.contentKey))
}()
case *packet.ResourcePackChunkData:
pack, ok := r.downloadingPacks[pk.UUID]
if !ok {
// We haven't received a ResourcePackDataInfo packet from the server, so we can't use this data to
// download a resource pack.
return false, fmt.Errorf("resource pack chunk data for resource pack that was not being downloaded")
}
lastData := pack.buf.Len()+int(pack.chunkSize) >= int(pack.size)
if !lastData && uint32(len(pk.Data)) != pack.chunkSize {
// The chunk data didn't have the full size and wasn't the last data to be sent for the resource pack,
// meaning we got too little data.
return false, fmt.Errorf("resource pack chunk data had a length of %v, but expected %v", len(pk.Data), pack.chunkSize)
}
if pk.ChunkIndex != pack.expectedIndex {
return false, fmt.Errorf("resource pack chunk data had chunk index %v, but expected %v", pk.ChunkIndex, pack.expectedIndex)
}
pack.expectedIndex++
pack.newFrag <- pk.Data
case *packet.SetLocalPlayerAsInitialised:
if pk.EntityRuntimeID != r.gameData.EntityRuntimeID {
return false, fmt.Errorf("entity runtime ID mismatch: entity runtime ID in StartGame and SetLocalPlayerAsInitialised packets should be equal")
}
close(r.spawn)
return true, nil
}
return false, nil
}
proxy := NewProxy(logrus.StandardLogger())
proxy.Server = minecraft.NewConn()
game_started := false
i := 0
func (r *replayConnector) loop() {
gameStarted := false
defer r.Close()
for {
i += 1
var magic uint32 = 0
var packet_length uint32 = 0
var toServer bool = false
offset, _ := f.Seek(0, io.SeekCurrent)
if offset == size {
log.Info("Reached End")
return nil
}
binary.Read(f, binary.LittleEndian, &magic)
if magic != 0xAAAAAAAA {
logrus.Fatal("Wrong Magic")
}
binary.Read(f, binary.LittleEndian, &packet_length)
binary.Read(f, binary.LittleEndian, &toServer)
payload := make([]byte, packet_length)
n, err := f.Read(payload)
payload, toServer, err := r.readPacket()
if err != nil {
log.Error(err)
return nil
logrus.Error(err)
}
if n != int(packet_length) {
log.Errorf("Truncated %d", i)
return nil
if payload == nil {
return
}
var src, dst = r.RemoteAddr(), r.LocalAddr()
if toServer {
src, dst = r.LocalAddr(), r.RemoteAddr()
}
var magic2 uint32
binary.Read(f, binary.LittleEndian, &magic2)
if magic2 != 0xBBBBBBBB {
logrus.Fatal("Wrong Magic2")
}
pk_data, err := minecraft.ParseData(payload, proxy.Server)
pkData, err := minecraft.ParseData(payload, r, src, dst)
if err != nil {
return err
logrus.Error(err)
return
}
pks, err := pk_data.Decode(proxy.Server)
pks, err := pkData.Decode(r)
if err != nil {
log.Error(err)
logrus.Error(err)
continue
}
for _, pk := range pks {
logrus.Printf("%s", reflect.TypeOf(pk).String()[1:])
if game_started {
if packetCB != nil {
packetCB(pk, proxy, toServer)
}
if !gameStarted {
gameStarted, _ = r.handleLoginSequence(pk)
} else {
switch pk := pk.(type) {
case *packet.StartGame:
proxy.Server.SetGameData(minecraft.GameData{
WorldName: pk.WorldName,
WorldSeed: pk.WorldSeed,
Difficulty: pk.Difficulty,
EntityUniqueID: pk.EntityUniqueID,
EntityRuntimeID: pk.EntityRuntimeID,
PlayerGameMode: pk.PlayerGameMode,
PersonaDisabled: pk.PersonaDisabled,
CustomSkinsDisabled: pk.CustomSkinsDisabled,
BaseGameVersion: pk.BaseGameVersion,
PlayerPosition: pk.PlayerPosition,
Pitch: pk.Pitch,
Yaw: pk.Yaw,
Dimension: pk.Dimension,
WorldSpawn: pk.WorldSpawn,
EditorWorld: pk.EditorWorld,
WorldGameMode: pk.WorldGameMode,
GameRules: pk.GameRules,
Time: pk.Time,
ServerBlockStateChecksum: pk.ServerBlockStateChecksum,
CustomBlocks: pk.Blocks,
Items: pk.Items,
PlayerMovementSettings: pk.PlayerMovementSettings,
ServerAuthoritativeInventory: pk.ServerAuthoritativeInventory,
Experiments: pk.Experiments,
ClientSideGeneration: pk.ClientSideGeneration,
ChatRestrictionLevel: pk.ChatRestrictionLevel,
DisablePlayerInteractions: pk.DisablePlayerInteractions,
})
game_started = true
if onConnect != nil {
onConnect(proxy)
}
}
r.packets <- pk
}
}
}
}
func createReplayConnector(filename string, packetFunc PacketFunc) (r *replayConnector, err error) {
r = &replayConnector{
pool: packet.NewPool(),
proto: minecraft.DefaultProtocol,
packetFunc: packetFunc,
spawn: make(chan struct{}),
close: make(chan struct{}),
packets: make(chan packet.Packet),
downloadingPacks: make(map[string]*downloadingPack),
}
logrus.Infof("Reading replay %s", filename)
r.f, err = os.Open(filename)
if err != nil {
return nil, err
}
stat, err := r.f.Stat()
if err != nil {
return nil, err
}
r.totalSize = stat.Size()
err = r.readHeader()
if err != nil {
return nil, err
}
go r.loop()
return r, nil
}
func (r *replayConnector) Close() error {
r.once.Do(func() {
close(r.close)
close(r.packets)
})
return nil
}
func (r *replayConnector) Authenticated() bool {
return true
}
func (r *replayConnector) ChunkRadius() int {
return 80
}
func (r *replayConnector) ClientCacheEnabled() bool {
return false
}
func (r *replayConnector) ClientData() login.ClientData {
return r.clientData
}
func (r *replayConnector) DoSpawn() error {
return r.DoSpawnContext(context.Background())
}
func (r *replayConnector) DoSpawnContext(ctx context.Context) error {
select {
case <-r.close:
return errors.New("do spawn")
case <-ctx.Done():
return errors.New("do spawn")
case <-r.spawn:
// Conn was spawned successfully.
return nil
}
}
func (r *replayConnector) DoSpawnTimeout(timeout time.Duration) error {
c, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return r.DoSpawnContext(c)
}
func (r *replayConnector) Flush() error {
return nil
}
func (r *replayConnector) GameData() minecraft.GameData {
return r.gameData
}
func (r *replayConnector) IdentityData() login.IdentityData {
return login.IdentityData{}
}
func (r *replayConnector) Latency() time.Duration {
return 0
}
func (r *replayConnector) LocalAddr() net.Addr {
return &net.UDPAddr{
IP: net.IPv4(1, 1, 1, 1),
}
}
func (r *replayConnector) Read(b []byte) (n int, err error) {
return 0, errors.New("not Implemented")
}
func (r *replayConnector) ReadPacket() (pk packet.Packet, err error) {
select {
case <-r.close:
return nil, net.ErrClosed
case p, ok := <-r.packets:
if !ok {
err = net.ErrClosed
}
return p, err
}
}
func (r *replayConnector) Write(b []byte) (n int, err error) {
return 0, errors.New("not Implemented")
}
func (r *replayConnector) WritePacket(pk packet.Packet) error {
return nil
}
func (r *replayConnector) RemoteAddr() net.Addr {
return &net.UDPAddr{
IP: net.IPv4(2, 2, 2, 2),
}
}
func (r *replayConnector) ResourcePacks() []*resource.Pack {
return r.resourcePacks
}
func (r *replayConnector) SetGameData(data minecraft.GameData) {
r.gameData = data
}
func (r *replayConnector) StartGame(data minecraft.GameData) error {
return r.StartGameContext(context.Background(), data)
}
func (r *replayConnector) StartGameContext(ctx context.Context, data minecraft.GameData) error {
return nil
}
func (r *replayConnector) StartGameTimeout(data minecraft.GameData, timeout time.Duration) error {
c, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return r.StartGameContext(c, data)
}
func (r *replayConnector) SetDeadline(t time.Time) error {
return nil
}
func (r *replayConnector) SetReadDeadline(t time.Time) error {
return nil
}
func (r *replayConnector) SetWriteDeadline(time.Time) error {
return nil
}
func (r *replayConnector) Pool() packet.Pool {
return r.pool
}
func (r *replayConnector) ShieldID() int32 {
return 0
}
func (r *replayConnector) Proto() minecraft.Protocol {
return r.proto
}
func (r *replayConnector) PacketFunc(header packet.Header, payload []byte, src, dst net.Addr) {
if r.packetFunc != nil {
r.packetFunc(header, payload, src, dst)
}
}

Binary file not shown.

View File

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

148
utils/skin.go Normal file
View File

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

165
utils/skinpack.go Normal file
View File

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

View File

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

View File

@ -1,18 +1,24 @@
// Package utils ...
package utils
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"crypto/aes"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sirupsen/logrus"
@ -20,35 +26,21 @@ import (
//"github.com/sandertv/gophertunnel/minecraft/gatherings"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
)
const SERVER_ADDRESS_HELP = `accepted server address formats:
123.234.123.234
123.234.123.234:19132
realm:<Realmname>
realm:<Realmname>:<Id>
`
var (
G_debug bool
G_preload_packs bool
G_interactive bool
)
var A string
func init() {
b, _ := base64.RawStdEncoding.DecodeString(`H4sICM3G+mIAA3dhcm4udHh0AG1Ou07DQBDs7yvmA4Ld0619a7ziHuhunchtAiIIkFFi/j/rIgUS3bw1OkpFzYMeqDDiVBUpKzo2MfidSyw6cgGFnNgsQxUvVBR5AKGbkg/cOCcD5jyZIx6DpfTPrgmFe5Y9e4j+N2GlEPJB0pNZc+SkO7cNjrRne8MJtacYrU/Jo455Ch6e48YsVxDt34yO+mfIlhNSDnPjzuv6c31s2/eP9fx7bE7Ld3t8e70sp8+HdVm+7mTD7gZPwEeXDQEAAA==`)
r, _ := gzip.NewReader(bytes.NewBuffer(b))
d, _ := io.ReadAll(r)
A = string(d)
var Options struct {
Debug bool
Preload bool
IsInteractive bool
ExtraDebug bool
Capture bool
PathCustomUserData string
}
var name_regexp = regexp.MustCompile(`\||(?:§.?)`)
var nameRegexp = regexp.MustCompile(`\||(?:§.?)`)
// cleans name so it can be used as a filename
// CleanupName cleans name so it can be used as a filename
func CleanupName(name string) string {
name = strings.Split(name, "\n")[0]
var _tmp struct {
@ -58,93 +50,67 @@ func CleanupName(name string) string {
if err == nil {
name = _tmp.K
}
name = string(name_regexp.ReplaceAll([]byte(name), []byte("")))
name = string(nameRegexp.ReplaceAll([]byte(name), []byte("")))
name = strings.TrimSpace(name)
return name
}
// connections
func ConnectServer(ctx context.Context, address string, ClientData *login.ClientData, want_packs bool, packetFunc PacketFunc) (serverConn *minecraft.Conn, err error) {
packet_func := func(header packet.Header, payload []byte, src, dst net.Addr) {
if G_debug {
PacketLogger(header, payload, src, dst)
}
if packetFunc != nil {
packetFunc(header, payload, src, dst)
}
}
func connectServer(ctx context.Context, address string, ClientData *login.ClientData, wantPacks bool, packetFunc PacketFunc) (serverConn *minecraft.Conn, err error) {
cd := login.ClientData{}
if ClientData != nil {
cd = *ClientData
}
logrus.Infof("Connecting to %s\n", address)
logrus.Info(locale.Loc("connecting", locale.Strmap{"Address": address}))
serverConn, err = minecraft.Dialer{
TokenSource: GetTokenSource(),
ClientData: cd,
PacketFunc: packet_func,
DownloadResourcePack: func(id uuid.UUID, version string) bool {
return want_packs
PacketFunc: packetFunc,
DownloadResourcePack: func(id uuid.UUID, version string, current int, total int) bool {
return wantPacks
},
}.DialContext(ctx, "raknet", address)
if err != nil {
return nil, err
return serverConn, err
}
logrus.Debug("Connected.")
Client_addr = serverConn.LocalAddr()
logrus.Debug(locale.Loc("connected", nil))
return serverConn, nil
}
func spawn_conn(ctx context.Context, clientConn *minecraft.Conn, serverConn *minecraft.Conn) error {
func spawnConn(ctx context.Context, clientConn minecraft.IConn, serverConn minecraft.IConn, gd minecraft.GameData) error {
wg := sync.WaitGroup{}
errs := make(chan error, 2)
if clientConn != nil {
wg.Add(1)
go func() {
defer wg.Done()
errs <- clientConn.StartGame(gd)
}()
}
wg.Add(1)
go func() {
errs <- clientConn.StartGame(serverConn.GameData())
}()
go func() {
defer wg.Done()
errs <- serverConn.DoSpawn()
}()
// wait for both to finish
wg.Wait()
for i := 0; i < 2; i++ {
select {
case err := <-errs:
if err != nil {
return fmt.Errorf("failed to start game: %s", err)
return errors.New(locale.Loc("failed_start_game", locale.Strmap{"Err": err}))
}
case <-ctx.Done():
return fmt.Errorf("connection cancelled")
return errors.New(locale.Loc("connection_cancelled", nil))
default:
}
}
return nil
}
// get longest line length
func max_len(lines []string) int {
o := 0
for _, line := range lines {
if o < len(line) {
o = len(line)
}
}
return o
}
// make text centered
func MarginLines(lines []string) string {
ret := ""
max := max_len(lines)
for _, line := range lines {
if len(line) != max {
ret += strings.Repeat(" ", max/2-len(line)/4)
}
ret += line + "\n"
}
return ret
}
// SplitExt splits path to filename and extension
func SplitExt(filename string) (name, ext string) {
name, ext = path.Base(filename), path.Ext(filename)
@ -163,3 +129,65 @@ func Clamp(a, b int) int {
}
return a
}
func RandSeededUUID(str string) string {
h := sha256.Sum256([]byte(str))
id, _ := uuid.NewRandomFromReader(bytes.NewBuffer(h[:]))
return id.String()
}
func WriteManifest(manifest *resource.Manifest, fpath string) error {
w, err := os.Create(path.Join(fpath, "manifest.json"))
if err != nil {
return err
}
e := json.NewEncoder(w)
e.SetIndent("", "\t")
if err = e.Encode(manifest); err != nil {
return err
}
return nil
}
func CfbDecrypt(data []byte, key []byte) []byte {
cipher, _ := aes.NewCipher([]byte(key))
shiftRegister := append(key[:16], data...)
iv := make([]byte, 16)
off := 0
for ; off < len(data); off += 1 {
cipher.Encrypt(iv, shiftRegister)
data[off] ^= iv[0]
shiftRegister = shiftRegister[1:]
}
return data
}
func abs(n float32) float32 {
if n < 0 {
n = -n
}
return n
}
func SizeofFmt(num float32) string {
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} {
if abs(num) < 1024.0 {
return fmt.Sprintf("%3.1f%sB", num, unit)
}
num /= 1024.0
}
return fmt.Sprintf("%.1fYiB", num)
}
func ShowFile(path string) {
path, _ = filepath.Abs(path)
if runtime.GOOS == "windows" {
cmd := exec.Command(`explorer`, "/select,", path)
cmd.Start()
return
}
if runtime.GOOS == "linux" {
}
}

View File

@ -2,26 +2,31 @@ package utils
import (
"archive/zip"
"fmt"
"compress/flate"
"io"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"strings"
"github.com/sirupsen/logrus"
)
func UnpackZip(r io.ReaderAt, size int64, unpack_folder string) {
func UnpackZip(r io.ReaderAt, size int64, unpackFolder string) {
zr, _ := zip.NewReader(r, size)
for _, src_file := range zr.File {
out_path := path.Join(unpack_folder, src_file.Name)
if src_file.Mode().IsDir() {
os.Mkdir(out_path, 0o755)
for _, srcFile := range zr.File {
srcName := strings.ReplaceAll(srcFile.Name, "\\", "/")
outPath := path.Join(unpackFolder, srcName)
if srcFile.Mode().IsDir() {
os.Mkdir(outPath, 0o755)
} else {
os.MkdirAll(path.Dir(out_path), 0o755)
fr, _ := src_file.Open()
f, _ := os.Create(path.Join(unpack_folder, src_file.Name))
os.MkdirAll(path.Dir(outPath), 0o755)
fr, _ := srcFile.Open()
f, _ := os.Create(path.Join(unpackFolder, srcName))
io.Copy(f, fr)
f.Close()
fr.Close()
}
}
}
@ -29,16 +34,22 @@ func UnpackZip(r io.ReaderAt, size int64, unpack_folder string) {
func ZipFolder(filename, folder string) error {
f, err := os.Create(filename)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
zw := zip.NewWriter(f)
// Register a custom Deflate compressor.
zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(out, flate.NoCompression)
})
err = filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error {
if !d.Type().IsDir() {
rel := path[len(folder)+1:]
zwf, _ := zw.Create(rel)
data, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
logrus.Error(err)
}
zwf.Write(data)
}