├── icon.png ├── launch.sh ├── src ├── biome_blob.nbt ├── misc.sh ├── log.sh ├── chunk.sh ├── palette.sh ├── hooks.sh ├── int.sh └── packet.sh ├── Dockerfile ├── demos ├── map.sh ├── seecret.sh ├── v.sh └── digmeout.sh ├── README.md └── mc.sh /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdomi/witchcraft/HEAD/icon.png -------------------------------------------------------------------------------- /launch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ncat -l -p 25565 -k -c "./mc.sh $1" 4 | -------------------------------------------------------------------------------- /src/biome_blob.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdomi/witchcraft/HEAD/src/biome_blob.nbt -------------------------------------------------------------------------------- /src/misc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # misc.sh - helper functions 3 | 4 | function repeat() { 5 | printf -- "$2%.0s" $(seq 1 $1) 6 | } 7 | 8 | function unhex() { 9 | xxd -p -r -c999999 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge 2 | 3 | RUN echo -e "http://alpine.sakamoto.pl/alpine/edge/main\nhttp://alpine.sakamoto.pl/alpine/edge/community" > /etc/apk/repositories \ 4 | && apk update \ 5 | && apk upgrade \ 6 | && apk add sed grep nmap-ncat bash alpine-base chrony file 7 | 8 | WORKDIR /witchcraft 9 | COPY . . 10 | 11 | EXPOSE 25565 12 | 13 | CMD ["/witchcraft/launch.sh"] 14 | -------------------------------------------------------------------------------- /src/log.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # log.sh - logging functions 3 | 4 | function log() { 5 | echo "[INFO] $@" >&2 6 | } 7 | function warn() { 8 | echo "[WARN] $@" >&2 9 | } 10 | function err() { 11 | echo "[FAIL] $@" >&2 12 | } 13 | function chatlog() { 14 | echo "[CHAT] $@" >&2 15 | } 16 | function hexlog() { 17 | echo -n "$@" | xxd >&2 18 | } 19 | function rhexlog() { 20 | echo -n "$@" | unhex | xxd >&2 21 | } 22 | -------------------------------------------------------------------------------- /demos/map.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # map.sh - simple map modification showcase 3 | 4 | function hook_chunks() { 5 | chunk_header 6 | for (( i=0; i<4096; i++ )); do 7 | chunk+="$(printf '%02x' $((RANDOM%30)))" 8 | done 9 | chunk_footer 10 | echo "$chunk" > $TEMP/world/0000000000000000 11 | 12 | pkt_chunk FFFFFFFF FFFFFFFF 00 13 | pkt_chunk FFFFFFFF 00000000 00 14 | pkt_chunk FFFFFFFF 00000001 00 15 | 16 | pkt_chunk 00000000 FFFFFFFF 00 17 | pkt_chunk 00000000 00000000 18 | pkt_chunk 00000000 00000001 00 19 | 20 | pkt_chunk 00000001 FFFFFFFF 00 21 | pkt_chunk 00000001 00000000 00 22 | pkt_chunk 00000001 00000001 00 23 | } 24 | -------------------------------------------------------------------------------- /src/chunk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # chunk.sh - misc chunk functions 3 | 4 | function chunk_header() { 5 | chunk="7fff" # amount of blocks, doesnt matter 6 | chunk+="08" # palette - bits per block 7 | chunk+="$(int2varint ${#palette[@]})" # palette - entries amount 8 | 9 | chunk+="${palette[@]}" 10 | 11 | chunk+="8004" # len of next array 12 | } 13 | 14 | function chunk_footer() { 15 | chunk+="0001" # biome palette 16 | chunk+="$(repeat 26 "0000000000000001")" # set biome to plains 17 | } 18 | 19 | function chunkfix() { 20 | sed -E 's/(.{8})(.{8})/\2\1/;' 21 | } 22 | 23 | function hexchunkfix() { 24 | sed -E 's/(.{16})(.{16})/\2\1/;' 25 | } 26 | -------------------------------------------------------------------------------- /src/palette.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # palette.sh - up to 255 block definitions 3 | # maps first 32 IDs to pre-flattening IDs, just for fun; rest is rather random 4 | # empty blocks (00) is what I can't easily implement :( 5 | # https://minecraft.fandom.com/wiki/Java_Edition_data_values/Pre-flattening/Block_IDs 6 | 7 | palette=() 8 | 9 | palette+=("00") # air 10 | palette+=("01") # stone 11 | palette+=("09") # grass block 12 | palette+=("0a") # dirt 13 | palette+=("0e") # cobblestone 14 | palette+=("0f") # planks 15 | palette+=("15") # sapling 16 | palette+=("21") # bedrock 17 | palette+=("00") # 18 | palette+=("31") # water 19 | palette+=("00") # 20 | palette+=("41") # lava 21 | palette+=("42") # sand 22 | palette+=("44") # gravel 23 | palette+=("45") # gold ore 24 | palette+=("47") # iron ore 25 | 26 | palette+=("49") # coal ore 27 | palette+=("4d") # wood 28 | palette+=("9401") # leaves 29 | palette+=("8402") # sponge 30 | palette+=("8602") # glass 31 | palette+=("8702") # lapis ore 32 | palette+=("8902") 33 | palette+=("00") # 34 | palette+=("9602") # sandstone 35 | palette+=("00") # 36 | palette+=("00") # 37 | palette+=("00") # 38 | palette+=("00") # 39 | palette+=("00") # 40 | palette+=("f60a") # tallgrass 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Witchcraft - a Minecraft server, written in Bash 2 | 3 | Requires busybox 1.35.0 or later (crashes on 1.34, for some reason), bash, gnu grep, gnu sed and nmap-ncat. Grep was actually only used once, so maybe I could make it work with the bb one? 4 | 5 | ## How to use 6 | 7 | To get the "plain" experience: 8 | 9 | ``` 10 | ./launch.sh 11 | ``` 12 | 13 | If it doesn't work (tested only on Alpine Edge), go for the docker route: 14 | 15 | ``` 16 | docker build -t witchcraft . 17 | docker run --rm -it -p25565:25565 witchcraft ./launch.sh 18 | ``` 19 | 20 | The above would launch the basic server without any plugins. You should check out the digmeout demo tho, it's kinda addictive: 21 | 22 | ``` 23 | ./launch.sh demos/digmeout.sh 24 | ``` 25 | 26 | ### What works 27 | 28 | - joining the game 29 | - chat 30 | - hooks (you can write your own.. plugins? check out `demos/`) 31 | - breaking blocks, to some extent 32 | - serializing and sending chunks 33 | - sending effects 34 | - possibly more 35 | 36 | ### What does not 37 | 38 | - multiple players (server tries to send the movement packets, but I haven't finished it) 39 | - breaking blocks with tools (client acts as if there isn't a tool in your hand) 40 | - capitalism (IRL; I wouldn't want to try implementing it here) 41 | 42 | -------------------------------------------------------------------------------- /demos/seecret.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # this example requires 'libqrencode' 3 | 4 | palette+=("af0b") # black wool 5 | palette+=("a00b") # white wool 6 | 7 | 8 | data="$(qrencode -t ASCII "$(base64 -d <<< "aHR0cHM6Ly95b3V0dS5iZS9kUXc0dzlXZ1hjUQ==")" | grep -vP '^ *$' | sed -E 's/##/#/g;s/ / /g;s/^ //')" 9 | m="$(echo -n "$data" | cut -c 1-16 | head -n 16)" 10 | m2="$(echo -n "$data" | cut -c 17- | head -n 16 | sed -E 's/$/ /g')" 11 | m3="$(echo -n "$data" | cut -c 1-16 | tail -n 9)" 12 | m4="$(echo -n "$data" | cut -c 17- | tail -n 9 | sed -E 's/$/ /g')" 13 | 14 | function hook_chunks() { 15 | rm -R $TEMP/world/* 16 | chunk_header 17 | chunk+=$(echo -n "$m2" | chunkfix | sed -E 's/#/1f/g;s/ /20/g') 18 | chunk+=$(repeat $((4096-256)) 00) 19 | chunk_footer 20 | echo "$chunk" > $TEMP/world/0000000000000000 21 | 22 | chunk_header 23 | chunk+=$(echo -n "$m4" | chunkfix | sed -E 's/#/1f/g;s/ /20/g') 24 | chunk+=$(repeat $((4096-256)) 00) 25 | chunk+=$(repeat $((9*16)) 00) 26 | chunk_footer 27 | echo "$chunk" > $TEMP/world/0000000000000001 28 | 29 | chunk_header 30 | chunk+=$(echo -n "$m" | chunkfix | sed -E 's/#/1f/g;s/ /20/g') 31 | chunk+=$(repeat $((4096-256)) 00) 32 | chunk_footer 33 | echo "$chunk" > $TEMP/world/0000000100000000 34 | 35 | chunk_header 36 | chunk+=$(echo -n "$m3" | chunkfix | sed -E 's/#/1f/g;s/ /20/g') 37 | chunk+=$(repeat $((4096-256)) 00) 38 | chunk+=$(repeat $((9*16)) 00) 39 | chunk_footer 40 | echo "$chunk" > $TEMP/world/0000000100000001 41 | 42 | pkt_chunk FFFFFFFF FFFFFFFF 00 43 | pkt_chunk FFFFFFFF 00000000 00 44 | pkt_chunk FFFFFFFF 00000001 00 45 | pkt_chunk FFFFFFFF 00000002 00 46 | 47 | pkt_chunk 00000000 FFFFFFFF 00 48 | pkt_chunk 00000000 00000000 00 49 | pkt_chunk 00000000 00000001 00 50 | pkt_chunk 00000000 00000002 00 51 | 52 | pkt_chunk 00000001 FFFFFFFF 00 53 | pkt_chunk 00000001 00000000 00 54 | pkt_chunk 00000001 00000001 00 55 | pkt_chunk 00000001 00000002 00 56 | 57 | pkt_chunk 00000002 FFFFFFFF 00 58 | pkt_chunk 00000002 00000000 00 59 | pkt_chunk 00000002 00000001 00 60 | pkt_chunk 00000002 00000002 00 61 | } 62 | -------------------------------------------------------------------------------- /src/hooks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # hooks.sh - dummy hooks functions 3 | 4 | spawn_pos=(0 0 0) # default spawn position 5 | gamemode=01 # creative 6 | 7 | # on player dig 8 | function hook_dig() { 9 | log "received Player Dig" 10 | rhexlog $a 11 | } 12 | 13 | # on player arm swing 14 | function hook_swing() { 15 | log "received Arm Swing" 16 | } 17 | 18 | # on player block placement 19 | function hook_block() { 20 | log "received Player Block Placement" 21 | rhexlog $a 22 | } 23 | 24 | # on chat message 25 | # msg available as $msg 26 | function hook_chat() { 27 | chatlog "$nick: $msg" 28 | pkt_chatmessage "$nick: $msg" "0000000000000000000000000000$eid" 29 | pkt_chatmessage "$nick: $msg" "0000000000000000000000000000$eid" > $TEMP/players/$nick/broadcast 30 | } 31 | 32 | # on move/move+rotation/rotation 33 | # X/Y/Z available as ${pos[0]} through ${pos[2]} 34 | function hook_move() { 35 | log "received Player Position" 36 | } 37 | 38 | # after login, join; intended for loading chunks 39 | function hook_chunks() { 40 | pkt_chunk FFFFFFFF FFFFFFFF 41 | pkt_chunk FFFFFFFF 00000000 42 | pkt_chunk FFFFFFFF 00000001 43 | 44 | pkt_chunk 00000000 FFFFFFFF 45 | pkt_chunk 00000000 00000000 46 | pkt_chunk 00000000 00000001 47 | 48 | pkt_chunk 00000001 FFFFFFFF 49 | pkt_chunk 00000001 00000000 50 | pkt_chunk 00000001 00000001 51 | 52 | pkt_chunk FFFFFFFF 00000002 53 | pkt_chunk 00000000 00000002 54 | pkt_chunk 00000001 00000002 55 | } 56 | 57 | # after spawning the player 58 | function hook_start() { 59 | : 60 | } 61 | 62 | # during login; useful for disabling all multiplayer functionality 63 | function hook_keepalive() { 64 | keep_alive & 65 | # sleep probably only needed for testing 66 | sleep 1 && position_delta & 67 | sleep 1 && handle_broadcast & 68 | pkt_chatmessage "+ $nick" "00000000000000000000000000000000" > $TEMP/players/$nick/broadcast 69 | } 70 | 71 | # during server ping; allows you to respond with a custom message. 72 | function hook_ping() { 73 | #json='{"version":{"name":"1.18.1","protocol":757},"players":{"max":100,"online":5,"sample":[{"name":"uwu","id":"4566e69f-c907-48ee-8d71-d7ba5aa00d20"}]},"description":{"text":"Hello world"}}' 74 | json='{"version":{"name":"§a§kaaa§aUwU§kaaa","protocol":756},"players":{"max":1,"online":0,"sample":[]},"description":{"text":"§aUwU"},"favicon":"data:image/png;base64,'"$(base64 -w0 icon.png)"'"}' 75 | res="$(str_len "$json")$(echo -n "$json" | xxd -p)" 76 | send_packet "00" "$res" 77 | } 78 | 79 | # on defining inventory contents 80 | function hook_inventory() { 81 | items=() 82 | pkt_inventory items 83 | } 84 | -------------------------------------------------------------------------------- /src/int.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # int.sh - int conversion functions, and other numeric functions 3 | 4 | # int2varint(int) 5 | function int2varint() { 6 | # very proud of my implementation; haven't seen anyone use a modulo for this ;p 7 | local a 8 | local b 9 | local c 10 | local out 11 | out=$(printf '%02x' "$1") 12 | if [[ $1 -lt 128 ]]; then 13 | : 14 | elif [[ $1 -lt 16384 ]]; then 15 | a=$(($1%128)) 16 | b=$(($1/128)) 17 | out=$(printf "%02x" $((a+128)))$(printf "%02x" $b) 18 | elif [[ $1 -lt $((128*128*128)) ]]; then 19 | a=$(($1%128)) 20 | c=$((($1/128)%128)) 21 | b=$(($1/16384)) 22 | out=$(printf "%02x" $((a+128)))$(printf "%02x" $((c+128)))$(printf "%02x" $b) 23 | fi 24 | echo -n "$out" 25 | } 26 | 27 | # varint2int() <<< varint 28 | function varint2int() { 29 | local x 30 | local uwu 31 | local out 32 | out="" 33 | x=1 34 | while true; do 35 | uwu=$(dd count=1 bs=1 status=none | xxd -p) 36 | 37 | out=$((out+((0x$uwu&127)*x))) 38 | x=$((x*128)) 39 | if [[ $((0x$uwu>>7)) == 0 ]]; then 40 | break 41 | fi 42 | done 43 | echo -n "$out" 44 | } 45 | 46 | # decode_position(Position) 47 | # https://wiki.vg/Protocol#Position 48 | function decode_position() { 49 | x=$((0x$1 >> 38)) 50 | y=$((0x$1 & 0xFFF)) 51 | z=$(((0x$1 >> 12) & 0x3FFFFFF)) 52 | 53 | [[ $x -gt 33554431 ]] && x=$((x-67108864)) 54 | [[ $y -gt 2047 ]] && y=$((y-4095)) 55 | [[ $z -gt 33554431 ]] && z=$((z-67108864)) 56 | } 57 | 58 | # encode_position(x, y, z) 59 | function encode_position() { 60 | local x 61 | local y 62 | local z 63 | 64 | x=$1 65 | y=$2 66 | z=$3 67 | 68 | [[ $x -lt 33554433 ]] && x=$((x+67108864)) 69 | [[ $y -lt 2049 ]] && y=$((y+4095)) 70 | [[ $z -lt 33554433 ]] && z=$((z+67108864)) 71 | 72 | printf "%016x" $((((x & 0x3FFFFFF)<<38) | ((z & 0x3FFFFFF)<<12) | (y & 0xFFF))) 73 | } 74 | 75 | # packet_len(packet) 76 | function packet_len() { 77 | int2varint $((($(echo -n "$1" | wc -c)+1))) 78 | } 79 | 80 | # hexpacket_len(hexpacket) 81 | function hexpacket_len() { 82 | int2varint $((($(echo -n "$1" | unhex | wc -c)+1))) 83 | } 84 | 85 | # str_len(string) 86 | function str_len() { 87 | int2varint $(echo -n "$1" | wc -c) 88 | } 89 | 90 | # hexstrl_len(hexstring) 91 | function hexstr_len() { 92 | int2varint $(echo -n "$1" | unhex | wc -c) 93 | } 94 | 95 | # hex2bin(hexstring) 96 | function hex2bin() { 97 | # \o/ 98 | echo -n "$1" | sed -E 's/0/0000/g;s/1/0001/g;s/2/0010/g;s/3/0011/g;s/4/0100/g;s/5/0101/g;s/6/0110/g;s/7/0111/g;s/8/1000/g;s/9/1001/g;s/a/1010/g;s/b/1011/g;s/c/1100/g;s/d/1101/g;s/e/1110/g;s/f/1111/g' 99 | } 100 | 101 | # from_ieee754(hexstring) 102 | # this works only on ints 103 | function from_ieee754() { 104 | local sign 105 | local exponent 106 | local asdf 107 | local exponent_ 108 | local val 109 | 110 | val=$(hex2bin "$1") 111 | sign=$(cut -c 1 <<< $val) 112 | exponent=$(cut -c 2-12 <<< $val) 113 | 114 | asdf=$(cut -c 13- <<< $val | sed -E 's/./,&/g;s/,//' | tr -d '\n' | awk -F , \ 115 | '{ 116 | power_count=-1 117 | x=0; 118 | for(i=1; i<=NF; i++) { 119 | x=(x + ($i * (2 ** power_count))) 120 | power_count=power_count-1; 121 | } 122 | print(x+1) 123 | }') 124 | 125 | exponent_=$((2#$exponent)) 126 | 127 | if [[ $sign == 0 ]]; then 128 | echo "$asdf $exponent_" | awk '{print (int($1 * (2 ** ($2 - 1023))))}' 129 | else 130 | echo "$asdf $exponent_" | awk '{print -(int($1 * (2 ** ($2 - 1023))))}' 131 | fi 132 | } 133 | 134 | # to_ieee754(number) 135 | # this works only on ints 136 | function to_ieee754() { 137 | local n 138 | local m 139 | local i 140 | local sign 141 | local mantissa 142 | 143 | n=$1 144 | m=2 145 | i=0 146 | sign=0 147 | 148 | if [[ $n -lt 0 ]]; then 149 | n=$((n-2*n)) 150 | sign=1 151 | fi 152 | 153 | while true; do 154 | if [[ $n -lt $((m*2)) ]]; then 155 | break 156 | fi 157 | i=$((i+1)) 158 | m=$((m*2)) 159 | done 160 | 161 | # we need to lie and tell Bash that this string is decimal, even though it's binary 162 | # otherwise, it will interpret this as octal 163 | 164 | # shh, nobody tell Bash 165 | mantissa=$(printf "1%010d" $((10#$(hex2bin $(printf "%x" $i))))) 166 | 167 | # leading zeroes kept screwing me over, I'm hardcoding 1 and -1.. and 0 168 | if [[ $n == -1 ]]; then 169 | printf "bff0000000000000" 170 | elif [[ $n == 1 ]]; then 171 | printf "3ff0000000000000" 172 | elif [[ $n == 0 ]]; then 173 | printf "0000000000000000" 174 | else 175 | res=$sign$mantissa$(printf "%0$((i+1))d" $(hex2bin $(printf "%x" $((n-m))) | sed -E 's/^0*//')) 176 | printf "%x" $((2#$res$(repeat $((64-$(echo -n $res | wc -c))) 0))) 177 | fi 178 | } 179 | 180 | # to_short(number) 181 | function to_short() { 182 | if [[ $1 -lt 0 && $1 -gt -32769 ]]; then 183 | printf "%04x" $(($1+65536)) 184 | elif [[ $1 -lt 32768 && $1 -gt -1 ]]; then 185 | printf "%04x" $1 186 | elif [[ $1 -lt -32768 ]]; then 187 | printf "8000" 188 | elif [[ $1 -gt 32767 ]]; then 189 | printf "7FFF" 190 | fi 191 | } 192 | -------------------------------------------------------------------------------- /demos/v.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # v.sh - a valentine card for my gfs <3 3 | 4 | palette+=("c10b") # red tulip 5 | palette+=("c20b") # orange tulip 6 | palette+=("c30b") # white tulip 7 | palette+=("00") # air (for the random gen) 8 | palette+=("00") # air 9 | palette+=("00") # air 10 | palette+=("e64b") # red concrete 11 | palette+=("f98001") # polished blackstone bricks 12 | palette+=("d84b") # white concrete 13 | palette+=("ae0b") # red wool 14 | 15 | spawn_pos=(8 -63 5) 16 | gamemode=00 17 | 18 | function hook_chunks() { 19 | 20 | if [[ $nick != 'selfisekai' && $nick != 'MaeIsBadAtDev' && $nick != 'Domi_UwU' ]]; then 21 | pkt_disconnect "§asomething something hmu if you wanna go in" 22 | fi 23 | 24 | rm -R $TEMP/world/* # regenerate chunks 25 | 26 | mmm=' 27 | ________________ 28 | ________________ 29 | ________________ 30 | ________________ 31 | ____^^____^^____ 32 | ___^..^__^..^___ 33 | __^.,..__....^__ 34 | __^....^^....^__ 35 | __^..........^__ 36 | __^..........^__ 37 | ___^........^___ 38 | ____^......^____ 39 | _____^....^_____ 40 | ______^..^______ 41 | ______@^^@______ 42 | _____@@@@@@_____ 43 | ________________' 44 | mmmap="$(echo -n "$mmm" | chunkfix | sed -E 's/\./25/g;s/#/02/g;s/,/27/g;s/_/00/g;s/@/26/g;s/\^/28/g')" 45 | chunk_header 46 | 47 | for (( y=0; y<16; y++ )); do 48 | if [[ $y == 0 ]]; then 49 | chunk+="$(repeat 256 "02")" 50 | elif [[ $y == 1 ]]; then 51 | for (( i=0; i<$((256-16)); i++ )); do 52 | chunk+=$(printf '%02x' $(((RANDOM%6)+31))) 53 | done 54 | b='' 55 | for i in {1..5}; do b+=$(printf '%02x' $(((RANDOM%6)+31))); done 56 | for i in {1..6}; do b+='26'; done 57 | for i in {1..5}; do b+=$(printf '%02x' $(((RANDOM%6)+31))); done 58 | chunk+="$(tr -d '\n' <<< "$b" | hexchunkfix)" 59 | else 60 | for (( x=0; x<16; x++ )); do 61 | if [[ $x == 15 ]]; then 62 | chunk+="$(echo -n "$mmmap" | tail -n $((y+1)) | head -n1)" 63 | else 64 | for (( z=0; z<16; z++ )); do 65 | chunk+="00" 66 | done 67 | fi 68 | done 69 | fi 70 | done 71 | 72 | 73 | chunk_footer 74 | 75 | echo "$chunk" > $TEMP/world/0000000000000000 76 | 77 | chunk_header 78 | chunk+="$(repeat 256 "02")" 79 | 80 | for (( i=0; i<256; i++ )); do 81 | chunk+="$(printf '%02x' $(((RANDOM%6)+31)))" 82 | done 83 | 84 | chunk+="$(repeat $((4096-512)) "00")" 85 | chunk_footer 86 | 87 | echo "$chunk" > $TEMP/world/FFFFFFFFFFFFFFFF 88 | echo "$chunk" > $TEMP/world/FFFFFFFF00000000 89 | echo "$chunk" > $TEMP/world/FFFFFFFF00000001 90 | 91 | echo "$chunk" > $TEMP/world/00000000FFFFFFFF 92 | echo "$chunk" > $TEMP/world/0000000000000001 93 | 94 | echo "$chunk" > $TEMP/world/00000001FFFFFFFF 95 | echo "$chunk" > $TEMP/world/0000000100000000 96 | echo "$chunk" > $TEMP/world/0000000100000001 97 | 98 | 99 | pkt_chunk FFFFFFFE FFFFFFFE 01 100 | pkt_chunk FFFFFFFE FFFFFFFF 01 101 | pkt_chunk FFFFFFFE 00000000 01 102 | pkt_chunk FFFFFFFE 00000001 01 103 | pkt_chunk FFFFFFFE 00000002 01 104 | 105 | pkt_chunk FFFFFFFF FFFFFFFE 01 106 | pkt_chunk FFFFFFFF FFFFFFFF 107 | pkt_chunk FFFFFFFF 00000000 108 | pkt_chunk FFFFFFFF 00000001 109 | pkt_chunk FFFFFFFF 00000002 110 | 111 | pkt_chunk 00000000 FFFFFFFE 01 112 | pkt_chunk 00000000 FFFFFFFF 113 | pkt_chunk 00000000 00000000 114 | pkt_chunk 00000000 00000001 115 | pkt_chunk 00000000 00000002 116 | 117 | pkt_chunk 00000001 FFFFFFFE 01 118 | pkt_chunk 00000001 FFFFFFFF 119 | pkt_chunk 00000001 00000000 120 | pkt_chunk 00000001 00000001 121 | pkt_chunk 00000001 00000002 122 | 123 | pkt_chunk 00000002 FFFFFFFE 01 124 | pkt_chunk 00000002 FFFFFFFF 125 | pkt_chunk 00000002 00000000 126 | pkt_chunk 00000002 00000001 127 | pkt_chunk 00000002 00000002 128 | 129 | pkt_particle ${spawn_pos[0]} ${spawn_pos[1]} ${spawn_pos[2]} 33 100 130 | } 131 | 132 | function async_particles() { 133 | while true; do 134 | pkt_particle 4 -60 12 33 10 135 | pkt_particle 12 -60 12 33 10 136 | sleep 0.5 137 | if [[ ! -a $TEMP/players/$nick ]]; then # die if disconnected 138 | break 139 | fi 140 | done 141 | } 142 | 143 | function hook_swing() { 144 | pkt_particle 4 -60 12 33 128 145 | pkt_particle 12 -60 12 33 128 146 | } 147 | 148 | function hook_ping() { 149 | json='{"version":{"name":"1.18.1","protocol":757},"players":{"max":1,"online":0,"sample":[]},"description":{"text":"§c♡♡♡§a h,,hi --><-- §c♡♡♡ §r \ncome see what I made?"},"favicon":"data:image/png;base64,'"$(base64 -w0 icon.png)"'"}' 150 | res="$(str_len "$json")$(echo -n "$json" | xxd -p)" 151 | send_packet "00" "$res" 152 | } 153 | 154 | function hook_start() { 155 | pkt_particle ${spawn_pos[0]} ${spawn_pos[1]} ${spawn_pos[2]} 33 10 156 | if [[ $nick == "selfisekai" ]]; then 157 | pkt_title "§dLauren §c<3" 158 | sleep 2 159 | pkt_title "Thanks for being here ^^" 160 | pkt_subtitle "You mean the world to me <3" 161 | elif [[ $nick == "MaeIsBadAtDev" ]]; then 162 | pkt_title "§aMaja §c<3" 163 | sleep 2 164 | pkt_title "Thanks for being awesome ^^" 165 | pkt_subtitle "I love you <3" 166 | fi 167 | async_particles & 168 | } 169 | -------------------------------------------------------------------------------- /demos/digmeout.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # digmeout - a score-based minigame 3 | # mine out as much ores before the time runs out 4 | 5 | time_left=30 6 | score=0 7 | spawn_pos=(8 -40 8) 8 | gamemode=00 9 | 10 | # palette expanded for easier randomization 11 | palette+=("45") # gold 12 | palette+=("47") # iron 13 | palette+=("49") # coal 14 | palette+=("8702") # lapis 15 | palette+=("d21a") # diamond 16 | palette+=("cf2a") # emerald 17 | palette+=("9a8b01") # copper 18 | palette+=("f11e") # redstone 19 | for i in {1..15}; do 20 | palette+=("01") # stone 21 | palette+=("02") # granite 22 | palette+=("04") # diorite 23 | done 24 | 25 | function hook_chunks() { 26 | local ore 27 | local plane 28 | 29 | rm -R $TEMP/world/* 30 | chunk_header 31 | 32 | oremap=() 33 | for (( y=0; y<16; y++ )); do 34 | plane='' 35 | for (( x=0; x<16; x++ )); do 36 | for (( z=0; z<16; z++ )); do 37 | ore=$(printf '%02x' $(((RANDOM%52)+31))) 38 | chunk+="$ore" 39 | plane+="$ore" 40 | done 41 | done 42 | oremap+=("$plane") 43 | done 44 | 45 | chunk_footer 46 | 47 | echo "${oremap[@]}" > $TEMP/players/$nick/oremap 48 | echo "$chunk" > $TEMP/world/0000000000000000 49 | 50 | pkt_chunk FFFFFFFF FFFFFFFF 00 51 | pkt_chunk FFFFFFFF 00000000 00 52 | pkt_chunk FFFFFFFF 00000001 00 53 | 54 | pkt_chunk 00000000 FFFFFFFF 00 55 | pkt_chunk 00000000 00000000 56 | pkt_chunk 00000000 00000001 00 57 | 58 | pkt_chunk 00000001 FFFFFFFF 00 59 | pkt_chunk 00000001 00000000 00 60 | pkt_chunk 00000001 00000001 00 61 | 62 | echo '0' > $TEMP/players/$nick/score 63 | pkt_title "Get, set..." 64 | } 65 | 66 | function hook_start() { 67 | timer & 68 | pkt_title "Go!" 69 | } 70 | 71 | function timer() { 72 | while true; do 73 | if [[ $time_left -le 0 ]]; then 74 | pos=$(cat $TEMP/players/$nick/position) 75 | pkt_effect $(awk -F, '{print $1}' <<< "$pos") $(awk -F, '{print $2}' <<< "$pos") $(awk -F, '{print $3}' <<< "$pos") 1032 76 | sleep 1 77 | break 78 | elif [[ $time_left -le 5 ]]; then 79 | sleep 1 80 | pos=$(cat $TEMP/players/$nick/position) 81 | pkt_effect $(awk -F, '{print $1}' <<< "$pos") $(awk -F, '{print $2}' <<< "$pos") $(awk -F, '{print $3}' <<< "$pos") 1000 82 | time_left=$((time_left-1)) 83 | elif [[ $time_left -le 10 ]]; then 84 | sleep 5 85 | time_left=$((time_left-5)) 86 | else 87 | sleep 10 88 | time_left=$((time_left-10)) 89 | fi 90 | if [[ ! -a $TEMP/players/$nick ]]; then # die if disconnected 91 | break 92 | fi 93 | pkt_chatmessage "§aTime left: §r${time_left}s" "00000000000000000000000000000000" 94 | done 95 | pkt_disconnect "Time's up! Your final score: §a$(cat $TEMP/players/$nick/score)" 96 | } 97 | 98 | function score() { 99 | # egh, async in Bash sucks 100 | score=$(cat $TEMP/players/$nick/score) 101 | if [[ "$block" == 45 ]]; then 102 | score=$((score+7)) 103 | elif [[ "$block" == 47 ]]; then 104 | score=$((score+5)) 105 | elif [[ "$block" == 49 ]]; then 106 | score=$((score+1)) 107 | elif [[ "$block" == 8702 ]]; then 108 | score=$((score+6)) 109 | elif [[ "$block" == d21a ]]; then 110 | score=$((score+25)) 111 | elif [[ "$block" == cf2a ]]; then 112 | score=$((score+15)) 113 | elif [[ "$block" == 9a8b01 ]]; then 114 | score=$((score+3)) 115 | elif [[ "$block" == f11e ]]; then 116 | score=$((score+15)) 117 | fi 118 | pkt_experience "$score" 119 | echo "$score" > $TEMP/players/$nick/score 120 | } 121 | 122 | function dig_async() { 123 | if [[ $block == 01 || $block == 02 || $block == 03 ]]; then # stone, granite... 124 | d=0.015 125 | elif [[ $block == 9a8b01 || $block == 49 ]]; then # copper, coal 126 | d=0.04 127 | else # everything else 128 | d=0.07 129 | fi 130 | for i in {1..9}; do 131 | if [[ $(cat $TEMP/players/$nick/mining) != "$x,$y,$z" ]]; then 132 | pkt_blockbreak $x $y $z ff 133 | break 134 | else 135 | pkt_blockbreak $x $y $z 0$i 136 | fi 137 | sleep $d 138 | done 139 | 140 | if [[ $(cat $TEMP/players/$nick/mining) == "$x,$y,$z" ]]; then 141 | pkt_diggingack $x $y $z 00 02 142 | score 143 | fi 144 | } 145 | 146 | function hook_dig() { 147 | ore=$(cat $TEMP/players/$nick/oremap | awk '{print $'"$((y+64))"'}' | sed -E 's/.{32}/&\n/g' | head -n $((z+1)) | tail -n 1 | sed -E 's/(.{16})(.{16})/\2\1/g;s/.{2}/&\n/g' | tail -n $((x+2)) | head -n1) 148 | block=${palette[$((0x$ore))]} 149 | if [[ $a == "1a02"* ]]; then # finished digging? 150 | score 151 | elif [[ $a == "1a00"* ]]; then 152 | echo "$x,$y,$z" > $TEMP/players/$nick/mining 153 | dig_async & 154 | elif [[ $a == "1a01"* ]]; then 155 | echo "catgirls" > $TEMP/players/$nick/mining 156 | fi 157 | } 158 | 159 | function hook_move() { 160 | if [[ ${pos[1]} -lt -96 ]]; then 161 | # utf-8 is actually only valid for strings created in the UTF world region 162 | # everything else is just sparkling unicode 163 | pkt_disconnect "$(unhex <<< "c2af5c5c5f28e38384295f2fc2af")" 164 | fi 165 | } 166 | 167 | function hook_keepalive() { 168 | keep_alive & 169 | } 170 | 171 | function hook_ping() { 172 | json='{"version":{"name":"1.18.1","protocol":757},"players":{"max":1,"online":0,"sample":[]},"description":{"text":"Minigame: §adigmeout§r | '"$time_left"' seconds per game"},"favicon":"data:image/png;base64,'"$(base64 -w0 icon.png)"'"}' 173 | res="$(str_len "$json")$(echo -n "$json" | xxd -p)" 174 | send_packet "00" "$res" 175 | } 176 | 177 | function hook_inventory() { 178 | items=($(repeat 36 "0 ")) 179 | items+=("721") 180 | pkt_inventory items 181 | } 182 | -------------------------------------------------------------------------------- /mc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | state='' 3 | dyed=0 4 | keepalive=0 5 | pos=(0 0 0) 6 | players=() 7 | time=18000 8 | TEMP=/dev/shm/witchcraft/ 9 | mkdir -p $TEMP $TEMP/players $TEMP/world 10 | 11 | source src/log.sh 12 | source src/int.sh 13 | source src/misc.sh 14 | source src/palette.sh 15 | source src/chunk.sh 16 | source src/packet.sh 17 | source src/hooks.sh 18 | 19 | if [[ -f "$1" ]]; then 20 | log "Loading plugin: $1" 21 | source "$1" 22 | fi 23 | 24 | function keep_alive() { 25 | while true; do 26 | sleep 1 27 | log "sending keepalive" 28 | echo '092100000000000000ff' | unhex 29 | # random data 30 | 31 | #res=$(printf '%016x' $time) 32 | #res=$res$res 33 | #log time: $res 34 | #time=$((time+240)) 35 | #echo -n "$(hexpacket_len "$res")59$res" | unhex 36 | done 37 | } 38 | 39 | function position_delta() { 40 | local deltaX 41 | local deltaY 42 | local deltaZ 43 | 44 | pos_old[0]=0 45 | pos_old[1]=0 46 | pos_old[2]=0 47 | 48 | while true; do 49 | sleep 0.1 50 | for i in $(ls "$TEMP/players/"); do 51 | if [[ "$i" != "$nick" ]]; then 52 | pos[0]=$(cat $TEMP/players/$i/position | awk -F, '{print($1)}') # idk, floating point broke 53 | pos[1]=$(cat $TEMP/players/$i/position | awk -F, '{print($2)}') 54 | pos[2]=$(cat $TEMP/players/$i/position | awk -F, '{print($3)}') 55 | log "posX: ${pos[0]}" 56 | deltaX=$((((${pos[0]}*32) - (${pos_old[0]}*32)) * 128)) 57 | deltaY=$(((${pos[1]}*32 - ${pos_old[1]}*32) * 128)) 58 | deltaZ=$(((${pos[2]}*32 - ${pos_old[2]}*32) * 128)) 59 | 60 | 61 | if [[ $deltaX != 0 || $deltaY != 0 || $deltaZ != 0 ]]; then 62 | pkt_position $deltaX $deltaY $deltaZ $(cat $TEMP/players/$i/eid) 63 | fi 64 | pos_old=("${pos[@]}") 65 | fi 66 | done 67 | done 68 | } 69 | 70 | function handle_broadcast() { 71 | while true; do 72 | for i in $(ls "$TEMP/players/"); do 73 | if [[ "$i" != "$nick" ]]; then 74 | if [[ -f $TEMP/players/$i/broadcast ]]; then 75 | packet="$(cat $TEMP/players/$i/broadcast)" 76 | if [[ "$last" != "$packet" ]]; then 77 | cat $TEMP/players/$i/broadcast 78 | last="$packet" 79 | fi 80 | else 81 | last='' 82 | fi 83 | fi 84 | done 85 | sleep 1 86 | done 87 | } 88 | 89 | function spawn_players() { 90 | for i in $(ls "$TEMP/players/"); do # fite me 91 | if [[ $i != $nick && ${players[@]} != *"$i"* ]]; then 92 | log "name: $i EID: $eid" 93 | pkt_playerinfo_add $i $(cat $TEMP/players/$i/eid) 94 | pkt_spawnplayer $(cat $TEMP/players/$i/eid) 95 | players+=("$nick") 96 | fi 97 | done 98 | } 99 | 100 | while true; do 101 | len=$(varint2int) 102 | 103 | a=$(dd count=$len bs=1 status=none | xxd -p) 104 | if [[ "$a" == '' ]]; then 105 | log "connection dyed" 106 | pkill -P $$ 107 | pkt_chatmessage "- $nick" "00000000000000000000000000000000" > $TEMP/players/$nick/broadcast 108 | sleep 0.3 109 | rm -R "$TEMP/players/$nick" 110 | exit 111 | fi 112 | 113 | if [[ $a == "00"* ]]; then 114 | log "responding to 00; state: $state" 115 | 116 | if [[ "$state" == '01' ]]; then 117 | log "status response" 118 | 119 | hook_ping 120 | 121 | state='' 122 | elif [[ "$state" == '02' ]]; then 123 | nick=$(cut -c 5- <<< "$a" | unhex | grep -Poh '[A-Za-z0-9_-]*') 124 | eid=$(printf "%04x" $RANDOM | cut -c 1-4) 125 | mkdir -p $TEMP/players/$nick 126 | echo -n $eid > $TEMP/players/$nick/eid 127 | log "login response" 128 | if [[ $keepalive == 0 ]]; then 129 | hook_keepalive 130 | keepalive=1 131 | fi 132 | 133 | # random uuid string len string (nick) 134 | res="0000000000000000000000000000$eid$(str_len "$nick")$(echo -n "$nick" | xxd -p)" 135 | send_packet "02" "$res" 136 | 137 | res="0100$eid" # entity id 138 | res+="00" # not hardcore 139 | res+="$gamemode" # survival mode 140 | res+="01" # ... as previously seen on Creative Mode (ignored) 141 | 142 | # I am *not* recreating the biome codecs 143 | res+="$(cat src/biome_blob.nbt | xxd -p) 144 | " 145 | #res+="01" # one dimension 146 | #res+="13$(echo -n "minecraft:overworld" | xxd -p)" 147 | #res+="0a000000" # dimension codec 148 | #res+="0a000000" # dimension 149 | #res+="13$(echo -n "minecraft:overworld" | xxd -p)" # dimension being spawned into 150 | #res+="0000000000000000" # beginning of sha256 of seed 151 | #res+="00" # max players (ignored) 152 | res+="0a" # view distance (min 2 chunks) 153 | res+="0a" # simulation distance 154 | res+="00" # reduced debug info? (false) 155 | res+="00" # enable respawn screen 156 | res+="00" # is debug (surprisingly, no) 157 | res+="01" # is flat (yeah, sure) 158 | 159 | send_packet "26" "$res" 160 | log "sent join game" 161 | #res="$(encode_position 10 10 10)" 162 | #res+="00000000" # angle as float 163 | 164 | #echo -n "$(hexpacket_len "$res")4B$res" | unhex 165 | #log "sent spawn position" 166 | 167 | hook_inventory 168 | hook_chunks 169 | pkt_pos ${spawn_pos[0]} ${spawn_pos[1]} ${spawn_pos[2]} 170 | spawn_players 171 | 172 | (sleep 1; hook_start) & 173 | state='' 174 | else 175 | if [[ $a == *"01" ]]; then # 01 - next state: status 176 | log 'set status' 177 | state='01' 178 | else # 02 - next state: login 179 | log 'set login' 180 | state='02' 181 | fi 182 | fi 183 | elif [[ $a == "01"* ]]; then 184 | log "responding to 01" 185 | echo "$len$a" | unhex 186 | log "bye" 187 | exit 188 | elif [[ $a == "0f"* ]]; then 189 | log "received keepalive" 190 | date "+%s" > $TEMP/players/$nick/ping 191 | elif [[ $a == "11"* ]]; then 192 | pos[0]=$(from_ieee754 $(cut -c 3-18 <<< "$a")) 193 | pos[1]=$(from_ieee754 $(cut -c 19-34 <<< "$a")) 194 | pos[2]=$(from_ieee754 $(cut -c 35-50 <<< "$a")) 195 | echo "${pos[0]},${pos[1]},${pos[2]}" > $TEMP/players/$nick/position 196 | hook_move 197 | elif [[ $a == "12"* ]]; then 198 | pos[0]=$(from_ieee754 $(cut -c 3-18 <<< "$a")) 199 | pos[1]=$(from_ieee754 $(cut -c 19-34 <<< "$a")) 200 | pos[2]=$(from_ieee754 $(cut -c 35-50 <<< "$a")) 201 | hook_move 202 | elif [[ $a == "13"* ]]; then 203 | log "received Player Rotation" 204 | elif [[ $a == "1a"* ]]; then 205 | decode_position $(cut -c 5-20 <<< "$a") 206 | hook_dig 207 | elif [[ $a == "2c"* ]]; then 208 | hook_swing 209 | elif [[ $a == "2e"* ]]; then 210 | hook_block 211 | elif [[ $a == "03"* ]]; then 212 | if [[ $((0x$(cut -c 3-4 <<< "$a"))) -lt 127 ]]; then # lazy varint check 213 | msg=$(cut -c 5- <<< "$a" | unhex) 214 | else 215 | msg=$(cut -c 3- <<< "$a" | unhex) 216 | fi 217 | hook_chat 218 | else 219 | log "unknown data from client" 220 | rhexlog "$a" 221 | fi 222 | 223 | done 224 | -------------------------------------------------------------------------------- /src/packet.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # packet.sh - play state packets 3 | 4 | # pkt_pos(x, y, z) 5 | function pkt_pos() { 6 | res="$(to_ieee754 $1)" # X 7 | res+="$(to_ieee754 $2)" # Y 8 | res+="$(to_ieee754 $3)" # Z 9 | res+="00000000" # yaw 10 | res+="00000000" # pitch 11 | res+="00" # bit field; all absolute 12 | res+="00" # teleport id (?) 13 | res+="00" # dismount vehicle? 14 | 15 | send_packet "38" "$res" 16 | log "sent player look and position" 17 | } 18 | 19 | # pkt_chunk(chunk_x, chunk_z, fill) 20 | function pkt_chunk() { 21 | # palettes are really cool once you figure them out :3 22 | # https://wiki.vg/Protocol#Chunk_Data_And_Update_Light 23 | 24 | local chunk 25 | local fill 26 | if [[ $3 == '' ]]; then 27 | fill='01' 28 | else 29 | fill="$3" 30 | fi 31 | 32 | res="$1" # chunk X 33 | res+="$2" # chunk Z 34 | 35 | # here goes the scary NBT field 36 | # nbt tag MOTION_BLOCKING len light data for a superflat map 37 | res+="0a00000c000f 4d4f54494f4e5f424c4f434b494e47 00000025 010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804 0000000020100804 00" 38 | 39 | if [[ -f $TEMP/world/$1$2 ]]; then 40 | chunk=$(cat $TEMP/world/$1$2) 41 | else 42 | chunk_header 43 | 44 | l=$(echo -n "8002" | unhex | varint2int) 45 | 46 | chunk+="$(repeat $((l*16)) "$fill")" 47 | 48 | chunk_footer 49 | echo -n "$chunk" > $TEMP/world/$1$2 50 | fi 51 | 52 | res+="$(hexstr_len "$chunk")" # Data len 53 | res+="$chunk" # Chunk data itself 54 | 55 | res+="00 01 00 00 00 00 00 00" # empty bitsets and light arrays 56 | 57 | send_packet "22" "$res" 58 | 59 | log "sending chunk data" 60 | } 61 | 62 | # pkt_effect(x, y, z, effect_id) 63 | function pkt_effect() { 64 | res="$(printf '%08x' $4)" 65 | res+="$(encode_position $1 $2 $3)" 66 | res+="00000000" 67 | res+="00" 68 | send_packet "23" "$res" 69 | log "sending effect" 70 | } 71 | 72 | # pkt_particle(x, y, z, particle_id, count) 73 | function pkt_particle() { 74 | res="$(printf '%08x' $4)" # particle id 75 | res+="01" # long distance 76 | res+="$(to_ieee754 $1)" # X 77 | res+="$(to_ieee754 $2)" # Y 78 | res+="$(to_ieee754 $3)" # Z 79 | res+="3f800000" # X offset 80 | res+="3f800000" # Y offset 81 | res+="3f800000" # Z offset 82 | res+="00000001" # particle data 83 | res+="$(printf '%08x' $5)" # particle count 84 | res+="" # data (left blank) 85 | send_packet "24" "$res" 86 | 87 | log "sending particle" 88 | } 89 | 90 | # pkt_playerinfo(name, eid) 91 | function pkt_playerinfo_add() { 92 | res="00" # add player 93 | res+="01" # total players 94 | res+="0000000000000000000000000000$2" # UUID 95 | 96 | res+="$(str_len "$1")$(echo -n "$1" | xxd -p)" 97 | 98 | res+="00" # array len; can be zero, but no skins then 99 | 100 | res+="01" # gamemode: creative 101 | res+="01" # ping: 1ms 102 | res+="00" # has display name: false 103 | 104 | send_packet "36" "$res" 105 | 106 | log "sent playerinfo" 107 | } 108 | 109 | # pkt_spawnplayer(eid) 110 | function pkt_spawnplayer() { 111 | res="$(int2varint $((0x$1)))" # entity id 112 | res+="0000000000000000000000000000$1" # a really badly made UUID 113 | res+="00 00 00 00 00 00 00 00" # X 114 | res+="00 00 00 00 00 00 00 00" # Y 115 | res+="00 00 00 00 00 00 00 00" # Z 116 | 117 | res+="00" # Angle (256 steps) 118 | res+="00" # Pitch (...) 119 | 120 | send_packet "04" "$res" 121 | log "sent spawnplayer" 122 | } 123 | 124 | # pkt_position(deltaX, deltaY, deltaZ, eid) 125 | function pkt_position() { 126 | local deltaX 127 | local deltaY 128 | local deltaZ 129 | local stepX 130 | local stepY 131 | local stepZ 132 | local n 133 | 134 | deltaX=$1 135 | deltaY=$2 136 | deltaZ=$3 137 | stepX=0 138 | stepY=0 139 | stepZ=0 140 | 141 | while true; do 142 | n=false 143 | if [[ $deltaX -gt 32767 ]]; then 144 | stepX=32767 145 | deltaX=$((deltaX-32767)) 146 | n=true 147 | fi 148 | if [[ $deltaX -lt -32768 ]]; then 149 | stepX=-32768 150 | deltaX=$((deltaX+32768)) 151 | n=true 152 | fi 153 | if [[ $deltaY -gt 32767 ]]; then 154 | stepY=32767 155 | deltaY=$((deltaY-32767)) 156 | n=true 157 | fi 158 | if [[ $deltaY -lt -32768 ]]; then 159 | stepY=-32768 160 | deltaY=$((deltaY+32768)) 161 | n=true 162 | fi 163 | if [[ $deltaZ -gt 32767 ]]; then 164 | stepZ=32767 165 | deltaX=$((deltaZ-32767)) 166 | n=true 167 | fi 168 | if [[ $deltaZ -lt -32768 ]]; then 169 | stepZ=-32768 170 | deltaZ=$((deltaZ+32768)) 171 | n=true 172 | fi 173 | 174 | [[ $n == false ]] && break 175 | 176 | pkt_position $stepX $stepY $stepZ 177 | done 178 | 179 | res="$(int2varint $((0x$4)))" # entity ID 180 | res+="$(to_short $deltaX)" 181 | res+="$(to_short $deltaY)" 182 | res+="$(to_short $deltaZ)" 183 | res+="00" # on ground 184 | send_packet "29" "$res" 185 | } 186 | 187 | # pkt_chatmessage(msg, sender_uuid) 188 | function pkt_chatmessage() { 189 | local msg 190 | local json 191 | local res 192 | 193 | msg=$(sed -E 's/"//g;s@\\@@g' <<< "$1") 194 | json='{"text":"'"$msg"'"}' 195 | res="$(str_len "$json")$(echo -n "$json" | xxd -p)" 196 | res+="00" # position: chat box 197 | res+="$2" 198 | 199 | send_packet "0f" "$res" 200 | } 201 | 202 | # pkt_title(msg) 203 | function pkt_title() { 204 | local txt 205 | txt='{"text":"'"$1"'"}' 206 | res="$(str_len "$txt")$(echo -n "$txt" | xxd -p)" 207 | send_packet "5a" "$res" 208 | } 209 | 210 | # pkt_subtitle(msg) 211 | function pkt_subtitle() { 212 | local txt 213 | txt='{"text":"'"$1"'"}' 214 | res="$(str_len "$txt")$(echo -n "$txt" | xxd -p)" 215 | send_packet "58" "$res" 216 | } 217 | 218 | # pkt_disconnect(reason) 219 | function pkt_disconnect() { 220 | txt='{"text":"'"$1"'"}' 221 | res="$(str_len "$txt")$(echo -n "$txt" | xxd -p)" 222 | send_packet "1a" "$res" 223 | 224 | pkill -P $$ 225 | pkt_chatmessage "- $nick" "00000000000000000000000000000000" > $TEMP/players/$nick/broadcast 226 | sleep 0.3 227 | rm -R "$TEMP/players/$nick" 228 | exit 229 | } 230 | 231 | # pkt_experience(lvl) 232 | function pkt_experience() { 233 | res="00000000" # experience bar 234 | res+="$(int2varint $1)" 235 | res+="00" 236 | send_packet "51" "$res" 237 | } 238 | 239 | # pkt_inventory(items) 240 | function pkt_inventory() { 241 | local -n _items=$1 242 | 243 | res="00" # inventory id 244 | res+="00" # state 245 | res+="$(int2varint ${#_items[@]})" # item count 246 | for i in ${!_items[@]}; do 247 | if [[ $i == 0 ]]; then 248 | res+="00" 249 | else 250 | res+="01 $(int2varint ${_items[$i]}) 01 00" 251 | fi 252 | done 253 | res+="01 00 01 00" 254 | 255 | send_packet "14" "$res" 256 | log "sent inventory" 257 | } 258 | 259 | # pkt_digginack(x, y, z, block, status) 260 | function pkt_diggingack() { 261 | res="$(encode_position $1 $2 $3)" 262 | res+="$4" 263 | res+="$5" 264 | res+="01" 265 | send_packet "08" "$res" 266 | log "sent dig ack" 267 | } 268 | 269 | # pkt_blockbreak(x, y, z, stage) 270 | function pkt_blockbreak() { 271 | res="$(int2varint $((0x$eid)))" 272 | res+="$(encode_position $1 $2 $3)" 273 | res+="$4" 274 | 275 | send_packet "09" "$res" 276 | } 277 | 278 | # pkt_soundeffect(x, y, z, id) 279 | # TODO: unbreak this 280 | function pkt_soundeffect() { 281 | res="$(int2varint $4)" # sound ID 282 | res+="05" # "block" category 283 | res+="$(printf '%08x' $(($1*8)))" # x 284 | res+="$(printf '%08x' $(($2*8)))" # y 285 | res+="$(printf '%08x' $(($3*8)))" # z 286 | res+="3f800000" # volume 287 | res+="3f800000" # pitch 288 | 289 | send_packet "5d" "$res" 290 | log "sound $(hexpacket_len "$res")5d$res" 291 | } 292 | 293 | # pkt_sendblock(x, y, z, id) 294 | function pkt_sendblock() { 295 | res="$(encode_position $1 $2 $3)" 296 | res+="$(int2varint $4)" 297 | 298 | send_packet "0c" "$res" 299 | } 300 | 301 | # send_packet(id, payload) 302 | function send_packet() { 303 | echo -n "$(hexpacket_len "$2")$1$2" | unhex 304 | } 305 | --------------------------------------------------------------------------------