commit e0b1d39d78acee1730104e61796ee07d068af3ea Author: Dominika Date: Wed Feb 9 03:58:56 2022 +0100 + work so far diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..4f8486a Binary files /dev/null and b/icon.png differ diff --git a/mc.sh b/mc.sh new file mode 100755 index 0000000..7ccf544 --- /dev/null +++ b/mc.sh @@ -0,0 +1,241 @@ +#!/bin/bash +state='' +dyed=0 +keepalive=0 +pos=(0 0 0) +players=() +TEMP=/dev/shm/witchcraft/ +mkdir -p $TEMP $TEMP/players $TEMP/world + +source src/log.sh +source src/int.sh +source src/packet.sh +source src/hooks.sh + +function keep_alive() { + while true; do + sleep 5 + log "sending keepalive" + echo '092100000000000000ff' | xxd -p -r + done +} + +function position_delta() { + local deltaX + local deltaY + local deltaZ + +# pos[0]=0 +# pos[1]=0 +# pos[2]=0 + pos_old[0]=0 + pos_old[1]=0 + pos_old[2]=0 + + while true; do + sleep 0.1 + for i in $(ls "$TEMP/players/"); do + if [[ "$i" != "$nick" ]]; then + pos[0]=$(cat $TEMP/players/$i/position | awk -F, '{print($1)}') # idk, floating point broke + pos[1]=$(cat $TEMP/players/$i/position | awk -F, '{print($2)}') + pos[2]=$(cat $TEMP/players/$i/position | awk -F, '{print($3)}') + log "posX: ${pos[0]}" + deltaX=$((((${pos[0]}*32) - (${pos_old[0]}*32)) * 128)) + deltaY=$(((${pos[1]}*32 - ${pos_old[1]}*32) * 128)) + deltaZ=$(((${pos[2]}*32 - ${pos_old[2]}*32) * 128)) + + + if [[ $deltaX != 0 || $deltaY != 0 || $deltaZ != 0 ]]; then + pkt_position $deltaX $deltaY $deltaZ $(cat $TEMP/players/$i/eid) + fi + pos_old=("${pos[@]}") + fi + done + done +} + +function handle_broadcast() { + while true; do + for i in $(ls "$TEMP/players/"); do + if [[ "$i" != "$nick" ]]; then + if [[ -f $TEMP/players/$i/broadcast ]]; then + packet="$(cat $TEMP/players/$i/broadcast)" + if [[ "$last" != "$packet" ]]; then + cat $TEMP/players/$i/broadcast + last="$packet" + fi + else + last='' + fi + fi + done + sleep 1 + done +} + +function spawn_players() { + for i in $(ls "$TEMP/players/"); do # fite me + if [[ $i != $nick && ${players[@]} != *"$i"* ]]; then + log "name: $i EID: $eid" + pkt_playerinfo_add $i $(cat $TEMP/players/$i/eid) + pkt_spawnplayer $(cat $TEMP/players/$i/eid) + players+=("$nick") + fi + done +} + +while true; do + len=$(varint2int) + + a=$(dd count=$len bs=1 status=none | xxd -p) + if [[ "$a" == '' ]]; then + log "connection dyed" + pkill -P $$ + pkt_chatmessage "- $nick" "00000000000000000000000000000000" > $TEMP/players/$nick/broadcast + sleep 1 + rm -R "$TEMP/players/$nick" + exit + fi + + if [[ -f /tmp/block ]]; then + res="$(encode_position 10 -10 10)" + res+="01" + + log "$res" + echo -n "$(hexpacket_len "$res")0c$res" | xxd -p -r + rm -R /tmp/block + fi + + if [[ -f /tmp/spawn ]]; then + spawn_players + rm /tmp/spawn + fi + + if [[ $a == "00"* ]]; then + log "responding to 00; state: $state" + + if [[ "$state" == '01' ]]; then + log "status response" + + #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"}}' + 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)"'"}' + res="$(str_len "$json")$(echo -n "$json" | xxd -p)" + echo "$(hexpacket_len "$res")00$res" | xxd -p -r + + state='' + elif [[ "$state" == '02' ]]; then + nick=$(cut -c 5- <<< "$a" | xxd -p -r | grep -Poh '[A-Za-z0-9_-]*') + eid=$(printf "%02x" $RANDOM) + mkdir -p $TEMP/players/$nick + echo -n $eid > $TEMP/players/$nick/eid + pkt_chatmessage "+ $nick" "00000000000000000000000000000000" > $TEMP/players/$nick/broadcast + log "login response" + if [[ $keepalive == 0 ]]; then + hook_keepalive + keepalive=1 + fi + + # random uuid string len string (nick) + res="0000000000000000000000000000$eid$(str_len "$nick")$(echo -n "$nick" | xxd -p)" + log "$(hexpacket_len "$res")02$res" + echo -n "$(hexpacket_len "$res")02$res" | xxd -p -r + + + res="$(encode_position 0 0 0)" + res+="00000000" # angle as float + + echo -n "$(hexpacket_len "$res")4B$res" | xxd -p -r + log "sent spawn position" + + #res="00000000" # entity id (0 or -2147483648, idk) + #res+="00" # not hardcore + #res+="00" # survival mode + #res+="01" # ... as previously seen on Creative Mode (ignored) + #res+="01" # one dimension + #res+="13$(echo -n "minecraft:overworld" | xxd -p)" + #res+="0a000000" # dimension codec + #res+="0a000000" # dimension + #res+="13$(echo -n "minecraft:overworld" | xxd -p)" # dimension being spawned into + #res+="0000000000000000" # beginning of sha256 of seed + #res+="0f" # max players (ignored) + #res+="02" # view distance (min 2 chunks) + #res+="02" # simulation distance + #res+="00" # reduced debug info? (false) + #res+="00" # enable respawn screen + #res+="00" # is debug (surprisingly, no) + #res+="01" # is flat (yeah, sure) + + #rhexlog "$(hexpacket_len "$res")26$res" + #echo -n "$(hexpacket_len "$res")26$res" | xxd -p -r + #log "sent join game" + + cat nbt_ + log "sent (hardcoded) join game" + + # send inventory (0x14) + res="00" # inventory id + res+="00" # state + res+="09" # item count + for i in {1..9}; do + res+="01 0$i 7f 00" # stone block + done + res+="01 00 01 00" # again, for held + + echo -n "$(hexpacket_len "$res")14$res" | xxd -p -r + log "sent inventory" + + pkt_pos + + source src/palette.sh + + hook_chunks + + spawn_players + + state='' + else + if [[ $a == *"01" ]]; then # 01 - next state: status + log 'set status' + state='01' + else # 02 - next state: login + log 'set login' + state='02' + fi + fi + elif [[ $a == "01"* ]]; then + log "responding to 01" + echo "$len$a" | xxd -p -r + log "bye" + exit + elif [[ $a == "0f"* ]]; then + log "received keepalive" + date "+%s" > $TEMP/players/$nick/ping + elif [[ $a == "11"* ]]; then + pos[0]=$(from_ieee754 $(cut -c 3-18 <<< "$a")) + pos[1]=$(from_ieee754 $(cut -c 19-34 <<< "$a")) + pos[2]=$(from_ieee754 $(cut -c 35-50 <<< "$a")) + echo "${pos[0]},${pos[1]},${pos[2]}" > $TEMP/players/$nick/position + hook_move + elif [[ $a == "12"* ]]; then + hook_move + elif [[ $a == "13"* ]]; then + hook_move + elif [[ $a == "1a"* ]]; then + hook_dig + elif [[ $a == "2c"* ]]; then + hook_swing + elif [[ $a == "2e"* ]]; then + hook_block + elif [[ $a == "03"* ]]; then + if [[ $((0x$(cut -c 3-4 <<< "$a"))) -lt 127 ]]; then # lazy varint check + msg=$(cut -c 5- <<< "$a" | xxd -p -r) + else + msg=$(cut -c 3- <<< "$a" | xxd -p -r) + fi + hook_chat + else + log "unknown data from client" + rhexlog "$a" + fi + +done diff --git a/nbt_ b/nbt_ new file mode 100644 index 0000000..87d7a55 Binary files /dev/null and b/nbt_ differ diff --git a/src/hooks.sh b/src/hooks.sh new file mode 100644 index 0000000..5e3b72a --- /dev/null +++ b/src/hooks.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# hooks.sh - dummy hooks functions + +# on player dig +function hook_dig() { + log "received Player Dig" + rhexlog $a +} + +# on player arm swing +function hook_swing() { + log "received Arm Swing" +} + +# on player block placement +function hook_block() { + log "received Player Block Placement" + rhexlog $a +} + +# on chat message +# msg available as $msg +function hook_chat() { + chatlog "$nick: $msg" + pkt_chatmessage "$nick: $msg" "0000000000000000000000000000$eid" + pkt_chatmessage "$nick: $msg" "0000000000000000000000000000$eid" > $TEMP/players/$nick/broadcast +} + +# on move/move+rotation/rotation +# X/Y/Z available as ${pos[0]} through ${pos[2]} +function hook_move() { + log "received Player Position" +} + +# after login, join; intended for loading chunks +function hook_chunks() { + pkt_chunk FFFFFFFF FFFFFFFF + pkt_chunk FFFFFFFF 00000000 + pkt_chunk FFFFFFFF 00000001 + + pkt_chunk 00000000 FFFFFFFF + pkt_chunk 00000000 00000000 + pkt_chunk 00000000 00000001 + + pkt_chunk 00000001 FFFFFFFF + pkt_chunk 00000001 00000000 + pkt_chunk 00000001 00000001 + + pkt_chunk FFFFFFFF 00000002 + pkt_chunk 00000000 00000002 + pkt_chunk 00000001 00000002 +} + +# during login; useful for disabling all multiplayer functionality +function hook_keepalive() { + keep_alive & + # sleep probably only needed for testing + sleep 1 && position_delta & + sleep 1 && handle_broadcast & +} diff --git a/src/int.sh b/src/int.sh new file mode 100644 index 0000000..378bb52 --- /dev/null +++ b/src/int.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# int.sh - int conversion functions, and other numeric functions + +# int2varint(int) +function int2varint() { + # very proud of my implementation; haven't seen anyone use a modulo for this ;p + local a + local b + local c + local out + out=$(printf '%02x' "$1") + if [[ $1 -lt 128 ]]; then + : + elif [[ $1 -lt 16384 ]]; then + a=$(($1%128)) + b=$(($1/128)) + out=$(printf "%02x" $((a+128)))$(printf "%02x" $b) + elif [[ $1 -lt $((128*128*128)) ]]; then + a=$(($1%128)) + c=$((($1/128)%128)) + b=$(($1/16384)) + out=$(printf "%02x" $((a+128)))$(printf "%02x" $((c+128)))$(printf "%02x" $b) + fi + echo -n "$out" +} + +# varint2int() <<< varint +function varint2int() { + local x + local uwu + local out + out="" + x=1 + while true; do + uwu=$(dd count=1 bs=1 status=none | xxd -p) + + out=$((out+((0x$uwu&127)*x))) + x=$((x*128)) + if [[ $((0x$uwu>>7)) == 0 ]]; then + break + fi + done + echo -n "$out" +} + +# parse_position(Position) +# https://wiki.vg/Protocol#Position +function parse_position() { + x=$((0x$1 >> 38)) + y=$((0x$1 & 0xFFF)) + z=$(((0x$1 >> 12) & 0x3FFFFFF)) + + [[ $x -gt 33554431 ]] && x=$((x-67108864)) + [[ $y -gt 2047 ]] && y=$((y-4095)) + [[ $z -gt 33554431 ]] && z=$((z-67108864)) +} + +# encode_position(x, y, z) +function encode_position() { + local x + local y + local z + + x=$1 + y=$2 + z=$3 + + [[ $x -lt 33554433 ]] && x=$((x+67108864)) + [[ $y -lt 2049 ]] && y=$((y+4095)) + [[ $z -lt 33554433 ]] && z=$((z+67108864)) + + printf "%016x" $((((x & 0x3FFFFFF)<<38) | ((z & 0x3FFFFFF)<<12) | (y & 0xFFF))) +} + +# packet_len(packet) +function packet_len() { + int2varint $((($(echo -n "$1" | wc -c)+1))) +} + +# hexpacket_len(hexpacket) +function hexpacket_len() { + int2varint $((($(echo -n "$1" | xxd -p -r | wc -c)+1))) +} + +# str_len(string) +function str_len() { + int2varint $(echo -n "$1" | wc -c) +} + +# hexstrl_len(hexstring) +function hexstr_len() { + int2varint $(echo -n "$1" | xxd -p -r | wc -c) +} + +# hex2bin(hexstring) +function hex2bin() { + # \o/ + 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' +} + +# from_ieee754(hexstring) +function from_ieee754() { + local sign + local exponent + local asdf + local exponent_ + local val + + val=$(hex2bin "$1") + sign=$(cut -c 1 <<< $val) + exponent=$(cut -c 2-12 <<< $val) + + asdf=$(cut -c 13- <<< $val | sed -E 's/./,&/g;s/,//' | tr -d '\n' | awk -F , \ + '{ + power_count=-1 + x=0; + for(i=1; i<=NF; i++) { + x=(x + ($i * (2 ** power_count))) + power_count=power_count-1; + } + print(x+1) + }') + + exponent_=$((2#$exponent)) + + if [[ $sign == 0 ]]; then + echo "$asdf $exponent_" | awk '{print (int($1 * (2 ** ($2 - 1023))))}' + else + echo "$asdf $exponent_" | awk '{print -(int($1 * (2 ** ($2 - 1023))))}' + fi +} + +# to_short(number) +function to_short() { + if [[ $1 -lt 0 && $1 -gt -32769 ]]; then + printf "%04x" $(($1+65536)) + elif [[ $1 -lt 32768 && $1 -gt -1 ]]; then + printf "%04x" $1 + elif [[ $1 -lt -32768 ]]; then + printf "8000" + elif [[ $1 -gt 32767 ]]; then + printf "7FFF" + fi +} diff --git a/src/log.sh b/src/log.sh new file mode 100644 index 0000000..807c120 --- /dev/null +++ b/src/log.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# log.sh - logging functions + +function log() { + echo "[INFO] $@" >&2 +} +function warn() { + echo "[WARN] $@" >&2 +} +function err() { + echo "[FAIL] $@" >&2 +} +function chatlog() { + echo "[CHAT] $@" >&2 +} +function hexlog() { + echo -n "$@" | xxd >&2 +} +function rhexlog() { + echo -n "$@" | xxd -p -r | xxd >&2 +} diff --git a/src/packet.sh b/src/packet.sh new file mode 100644 index 0000000..6d2c0ce --- /dev/null +++ b/src/packet.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# packet.sh - play state packets + +function pkt_pos() { + res="0000001000000000" # X + res+="0000000000000000" # Y + res+="0000000000000000" # Z + res+="00000000" # yaw + res+="00000000" # pitch + res+="00" # bit field; all absolute + res+="00" # teleport id (?) + res+="00" # dismount vehicle? + + echo -n "$(hexpacket_len "$res")38$res" | xxd -p -r + log "sent player look and position" +} + +function pkt_chunk() { + # palettes are really cool once you figure them out :3 + # https://wiki.vg/Protocol#Chunk_Data_And_Update_Light + + local chunk + + res="$1" # chunk X + res+="$2" # chunk Z + + # here goes the scary NBT field + # nbt tag MOTION_BLOCKING len light data for a superflat map + res+="0a00000c000f 4d4f54494f4e5f424c4f434b494e47 00000025 010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804010080402010080401008040201008040100804020100804 0000000020100804 00" + + chunk="ffff" # amount of blocks, doesnt matter + chunk+="08" # palette - bits per block + chunk+="$(int2varint ${#palette[@]})" # palette - entries amount + + chunk+="${palette[@]}" + #res+="0f af0b 01 00" + + chunk+="8002" # len of next array + l=$(echo -n "8002" | xxd -p -r | varint2int) + + for (( i=0; i<$((l*8)); i++ )); do + chunk+="01" # third entry of palette + done + + chunk+="0001" # biome palette + for i in {1..26}; do + chunk+="0000000000000001" # set biome + done + + res+="$(int2varint $(hexstr_len "$chunk"))" # Data len + res+="$chunk" # Chunk data itself + + res+="00 01 00 00 00 00 00 00" # empty bitsets and light arrays + + echo -n "$(hexpacket_len "$res")22$res" | xxd -p -r + + log "sending chunk data" +} + +# pkt_effect(x, y, z, effect_id) +function pkt_effect() { + res="$(printf '%08x' $4)" + res+="$(encode_position $1 $2 $3)" + res+="00000000" + res+="00" + echo -n "$(hexpacket_len "$res")23$res" | xxd -p -r + log "sending effect" +} + +# pkt_playerinfo(name, eid) +function pkt_playerinfo_add() { + res="00" # add player + res+="01" # total players + res+="0000000000000000000000000000$2" # random UUID + + res+="$(str_len "$1")$(echo -n "$1" | xxd -p)" + + res+="00" # array len; can be zero, but no skins then + + res+="01" # gamemode: creative + res+="01" # ping: 1ms + res+="00" # has display name: false + + echo -n "$(hexpacket_len "$res")36$res" | xxd -p -r + + log "sent playerinfo" +} + +# pkt_spawnplayer(eid) +function pkt_spawnplayer() { + res="$(int2varint $((0x$1)))" # entity id + res+="0000000000000000000000000000$1" # a really badly made UUID + res+="00 00 00 00 00 00 00 00" # X + res+="00 00 00 00 00 00 00 00" # Y + res+="00 00 00 00 00 00 00 00" # Z + + res+="00" # Angle (256 steps) + res+="00" # Pitch (...) + + echo -n "$(hexpacket_len "$res")04$res" | xxd -p -r + rhexlog "$(hexpacket_len "$res")04$res" + log "sent spawnplayer" +} + +# pkt_position(deltaX, deltaY, deltaZ, eid) +function pkt_position() { + local deltaX + local deltaY + local deltaZ + local stepX + local stepY + local stepZ + local n + + deltaX=$1 + deltaY=$2 + deltaZ=$3 + stepX=0 + stepY=0 + stepZ=0 + + while true; do + n=false + if [[ $deltaX -gt 32767 ]]; then + stepX=32767 + deltaX=$((deltaX-32767)) + n=true + fi + if [[ $deltaX -lt -32768 ]]; then + stepX=-32768 + deltaX=$((deltaX+32768)) + n=true + fi + if [[ $deltaY -gt 32767 ]]; then + stepY=32767 + deltaY=$((deltaY-32767)) + n=true + fi + if [[ $deltaY -lt -32768 ]]; then + stepY=-32768 + deltaY=$((deltaY+32768)) + n=true + fi + if [[ $deltaZ -gt 32767 ]]; then + stepZ=32767 + deltaX=$((deltaZ-32767)) + n=true + fi + if [[ $deltaZ -lt -32768 ]]; then + stepZ=-32768 + deltaZ=$((deltaZ+32768)) + n=true + fi + + [[ $n == false ]] && break + + pkt_position $stepX $stepY $stepZ + done + + res="$(int2varint $((0x$4)))" # entity ID + res+="$(to_short $deltaX)" + res+="$(to_short $deltaY)" + res+="$(to_short $deltaZ)" + res+="00" # on ground + echo -n "$(hexpacket_len "$res")29$res" | xxd -p -r +} + +# pkt_chatmessage(msg, sender_uuid) +function pkt_chatmessage() { + local msg + local json + local res + + msg=$(sed -E 's/"//g;s@\\@@g' <<< "$1") + json='{"text":"'"$msg"'"}' + res="$(str_len "$json")$(echo -n "$json" | xxd -p)" + res+="00" # position: chat box + res+="$2" + + echo -n "$(hexpacket_len "$res")0F$res" | xxd -p -r +} + +# pkt_title(msg) +function pkt_title() { + local txt + txt='{"text":"'"$1"'"}' + res="$(str_len "$txt")$(echo -n "$txt" | xxd -p)" + echo -n "$(hexpacket_len "$res")5a$res" | xxd -p -r +} diff --git a/src/palette.sh b/src/palette.sh new file mode 100644 index 0000000..8826015 --- /dev/null +++ b/src/palette.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# palette.sh - up to 255 block definitions +# maps first 32 IDs to pre-flattening IDs, just for fun; rest is rather random +# empty blocks (00) is what I can't easily implement :( +# https://minecraft.fandom.com/wiki/Java_Edition_data_values/Pre-flattening/Block_IDs + +palette=() + +palette+=("00") # air +palette+=("01") # stone +palette+=("09") # grass +palette+=("0a") # dirt +palette+=("0e") # cobblestone +palette+=("0f") # planks +palette+=("15") # sapling +palette+=("21") # bedrock +palette+=("00") # +palette+=("31") # water +palette+=("00") # +palette+=("41") # lava +palette+=("42") # sand +palette+=("44") # gravel +palette+=("45") # gold ore +palette+=("47") # iron ore + +palette+=("49") # coal ore +palette+=("4d") # wood +palette+=("9401") # leaves +palette+=("8402") # sponge +palette+=("8602") # glass +palette+=("8702") # lapis ore +palette+=("8902") +palette+=("00") # +palette+=("9602") # sandstone +palette+=("00") # +palette+=("00") # +palette+=("00") # +palette+=("00") # +palette+=("00") # +palette+=("f60a") # grass