ammaraskar / pyCraft

Minecraft-client networking library in Python
Other
814 stars 184 forks source link

How to implement falling? #111

Open CamronW opened 5 years ago

CamronW commented 5 years ago

Hey,

I've been struggling to work out how to properly implement falling. Right now the player floats in the air if the block is deleted below them. I can't find anything useful on the protocol wiki.

Thanks

TheSnoozer commented 5 years ago

If I remember correctly the server will send the player an Entity Velocity packet with a negative Z value if it floats. The negative Z essentially should tell the client to go down. The same packet is also being sent, when the player is being punched, getting pushed by water, or any other external force (not sure about pistons, but you get the point).

As far as i can tell the (new) requested position gets acknowledged by sending out a Player Position packet (search for Updates the player's XYZ position on the server. on the wiki). As far as I can see this client currently has the Player Position And Look (serverbound) implemented.

Now it's getting a bit more complicated, since the Player Position And Look Packet, would need your new position and your client entity id. The client entity id is part of the Join Game packet and your current position can be obtained by the Player Position And Look (clientbound).

Rough outline of the code:

def main():
    player_entity_id = 0
    player_x = 0
    player_y = 0
    player_z = 0
    player_yaw = 0
    player_pitch = 0

    # listen to JoinGamePacket
    def handle_join_game(join_game_packet):
        player_entity_id = join_game_packet.entity_id

    connection.register_packet_listener(
        handle_join_game, clientbound.play.JoinGamePacket)

    # listen to PlayerPositionAndLookPacket
    def handle_player_position_and_look(playerPositionAndLookPacket):
        player_x = playerPositionAndLookPacket.x
        player_y = playerPositionAndLookPacket.y
        player_z = playerPositionAndLookPacket.z
        player_yaw = playerPositionAndLookPacket.yaw
        player_pitch = playerPositionAndLookPacket.pitch

    connection.register_packet_listener(
        handle_player_position_and_look, clientbound.play.PlayerPositionAndLookPacket)

    # listen to EntityVelocityPacket
    def handle_entity_velocity(entity_velocity_packet):
        new_x = player_x + entity_velocity_packet.velocity_x
        new_y = player_y + entity_velocity_packet.velocity_y
        new_z = player_z + entity_velocity_packet.velocity_z

        # send an acknowledgement to the server
        position_response = serverbound.play.PositionAndLookPacket()
        position_response.x = new_x
        position_response.feet_y = new_y
        position_response.z = new_z
        position_response.yaw = player_yaw
        position_response.pitch = player_pitch
        self.connection.write_packet(position_response)

        # update local reference
        player_x = new_x
        player_y = new_y
        player_z = new_z

    connection.register_packet_listener(
        handle_join_game, clientbound.play.EntityVelocityPacket)

Consider this as free and unencumbered software (piece) released into the public domain, without warranty of any kind

CamronW commented 5 years ago

Hey,

I've done some testing and I believe the Entity Velocity Packet only accounts for things other than a player? For example a chicken falling displays the correct packet, however when the player falls no packets are sent.

Do you know of a way to get player velocity?

Thanks

CamronW commented 5 years ago

I believe the way Minecraft Console Client does it is by checking if the player is on ground by checking the block below them. However this would require loading in the chunk packets and finding the block below the player which seems overly complicated

TheSnoozer commented 5 years ago

Ahh thanks for pointing that out. You seem to be correct and the Velocity is only being sent when the player gets pushed by an external force.

I actually looked into the Minecraft Console Client and did some debugging and it seems, that it simply sends out a negative Y position regardless if it stands on a block or not. Based on the code it seems that what you mention is indeed the intended, but at least my short debugging session tells me something different.

In general I would agree it would be smarter if the client would only send the 'I want to go down', if there is no block below, but unfortunately pycraft does not have the chunk data implemented and thus is not aware of it's surroundings (would be a really great addition, but those packets are a pain).

I currently see two ways to solve this with pycraft.

  1. Either send out `serverbound.play.PositionAndLookPacket()``with a negative Y on a constant basis
  2. Slightly misuse how minecraft handels wrong movement. What I mean by that is, that minecraft will send out a clientbound.play.PlayerPositionAndLookPacket if you do a movement that would not be possible (e.g. a block in its way). You could technically implement a new command that sends a 'I want to go down' packet, until you receive a correcting clientbound.play.PlayerPositionAndLookPacket. You could also combine it with receiving chatmessage or whatever you want.

Rough sketch of 2:

class Player:
    x = 0
    y = 0
    z = 0
    yaw = 0
    pitch = 0
    last_y_pos = 0

def main():
    # ....

    # listen to PlayerPositionAndLookPacket
    def handle_player_position_and_look(playerPositionAndLookPacket):
        Player.x = playerPositionAndLookPacket.x
        Player.y = playerPositionAndLookPacket.y
        Player.z = playerPositionAndLookPacket.z
        Player.yaw = playerPositionAndLookPacket.yaw
        Player.pitch = playerPositionAndLookPacket.pitch
        Player.last_y_pos = playerPositionAndLookPacket.y

    connection.register_packet_listener(
        handle_player_position_and_look, clientbound.play.PlayerPositionAndLookPacket)

    # ....

    while True:
        try:
            text = input()
            if text == "/respawn":
                print("respawning...")
                packet = serverbound.play.ClientStatusPacket()
                packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN
                connection.write_packet(packet)
            elif text == "/down":
                import time
                Player.last_y_pos = 0
                while (int(Player.last_y_pos) - 1) != int(Player.y):
                    # send a packet every ~50ms
                    time.sleep(0.05)
                    Player.y -= 1
                    position_response = serverbound.play.PositionAndLookPacket()
                    position_response.on_ground = False
                    position_response.x = Player.x
                    position_response.feet_y = Player.y
                    position_response.z = Player.z
                    position_response.yaw = Player.yaw
                    position_response.pitch = Player.pitch
                    connection.write_packet(position_response)
            else:
                packet = serverbound.play.ChatPacket()
                packet.message = text
                connection.write_packet(packet)
        except KeyboardInterrupt:
            print("Bye!")
            sys.exit()

That code might need some tweaking (looks ugly), but lets the player 'fall' when you enter the command /down.

CamronW commented 5 years ago

Yeah I did try to implement chunk packets, I managed to get the data through but couldn't work out how to parse it.

For the last day or so I've been working on trying to get gravity to work, however I can't get it to work properly so that it doesn't get kicked by anticheats on the server. Setting the feet_y 1 block lower gets it kicked so I've been trying to find a way to replicate "vanilla" falling velocity.

Still trying, hopefully will get there soon aha

TheSnoozer commented 5 years ago

Yeah I did try to implement chunk packets, I managed to get the data through but couldn't work out how to parse it.

Maybe that would already be worth creating a merge request and integrate the relevant packets into this repo? I could take a look and help troubleshoot....

I can remember I tried to implement the chunk data myself and then lost interest in minecraft xD

CamronW commented 5 years ago

Yeah, I'll have to clean the code up a bit first - it's not looking that good at the moment aha. Once I've done that I'll create a merge request

Do you have any idea how to properly calculate the velocity of the player so it falls like normal?

TheSnoozer commented 5 years ago

I'm not a 100% certain what the exact values are, but I would recommend to look at other clients.

Here is the information I found while doing a very quick search:

And the actual implementations

That should give a good impression, how it works, if its 100% right, I guess you would only find out by looking at the actual java client code.

CamronW commented 5 years ago

I've tried to get this to work, I got the numbers to match that of Minecraft Console Client, however it still doesn't fall smoothly.

Here are the numbers I got when comparing my fall script with Minecraft Console Clients: Minecraft Console Client: https://hastebin.com/ujimibiluh.sql My Client: https://hastebin.com/taqubawoxi.sql The numbers are the same apart from some slight rounding, I can't see what can be wrong.

Can you see anything wrong with this code? I'm just trying to get it to fall 1 block normally for now


print("Trying to move down 1 block")
velocity = 0.08
newY = pos_look.y
motionY = 0
for x in range(5): #5 Because that's enough to get the Y to be down greater than 1 block
    print("Before position:", round(newY, 13))
    motionY = motionY - 0.08
    motionY = motionY * 0.9800000190734863
    print("Motion Y:", motionY)
    newY = newY + motionY
    print("End position:", round(newY, 13))
    connection.write_packet(PositionAndLookPacket(
        x        = pos_look.x,
        feet_y  = round(newY, 13),
        z        = pos_look.z,
        yaw    = pos_look.yaw,
        pitch    = pos_look.pitch,
        on_ground = True))
    time.sleep(0.05) #20 Ticks per second
TheSnoozer commented 5 years ago

Indeed the numbers seems about the same. Does your player fall smoothly with the Minecraft Console Client? If so, I guess the timing of sending out the new position could be off (e.g. the other client sends more packets in a shorter period). When do the packets get sent out with the Minecraft Console Client and your version?

For future reference (the Minecraft Console Client):

Before Position :89
Motion Y: -0.0784000015258789
End Position :88.9215999984741
Before Position :88.9215999984741
Motion Y: -0.155232004516602
End Position :88.7663679939575
Before Position :88.7663679939575
Motion Y: -0.230527368912964
End Position :88.5358406250446
Before Position :88.5358406250446
Motion Y: -0.304316827457544
End Position :88.231523797587
Before Position :88.231523797587
Motion Y: -0.376630498238655
End Position :87.8548932993484
Before Position :88
Motion Y: -0.447497896983418

and your client:

Before position: 89.0
Motion Y: -0.0784000015258789
End position: 88.9215999984741
Before position: 88.9215999984741
Motion Y: -0.1552320045166016
End position: 88.7663679939575
Before position: 88.7663679939575
Motion Y: -0.230527368912964
End position: 88.5358406250446
Before position: 88.5358406250446
Motion Y: -0.30431682745754424
End position: 88.231523797587
Before position: 88.231523797587
Motion Y: -0.37663049823865513
End position: 87.8548932993484
Before position: 87.8548932993484
Motion Y: -0.44749789698341763
CamronW commented 5 years ago

Yeah with Minecraft Console Client the player falls almost perfect, as if it was a vanilla client.

I added some timings to the Minecraft Console Client, here's what they look like.

12/08/2018 05:24:44.639: Before Position :89
12/08/2018 05:24:44.639: Motion Y: -0.0784000015258789
12/08/2018 05:24:44.641: End Position :88.9215999984741
12/08/2018 05:24:44.743: Before Position :88.9215999984741
12/08/2018 05:24:44.743: Motion Y: -0.155232004516602
12/08/2018 05:24:44.744: End Position :88.7663679939575
12/08/2018 05:24:44.745: Before Position :88.7663679939575
12/08/2018 05:24:44.745: Motion Y: -0.230527368912964
12/08/2018 05:24:44.746: End Position :88.5358406250446
12/08/2018 05:24:44.856: Before Position :88.5358406250446
12/08/2018 05:24:44.857: Motion Y: -0.304316827457544
12/08/2018 05:24:44.858: End Position :88.231523797587
12/08/2018 05:24:44.858: Before Position :88.231523797587
12/08/2018 05:24:44.858: Motion Y: -0.376630498238655
12/08/2018 05:24:44.859: End Position :87.8548932993484
12/08/2018 05:24:44.961: Before Position :88
12/08/2018 05:24:44.962: Motion Y: -0.447497896983418

As you can see, it sends out 5 packets, spanning over 219 miliseconds before it updated the "Before Position". This means it's running at a little under 20 tps, however I believe it's most likely running at 20 and that there are some inaccuracies with the timings.

My client is also running at 20~tps due to the time.sleep(0.05)

As far as I can see, the numbers match between my client and Minecraft Console Client. Unless I've calculated them wrong, the only thing I can think of is that perhaps Minecraft Console Client is sending out another packet?

TheSnoozer commented 5 years ago

Mhh interesting, I would try to investigate what packets are actually send by the pycraft client. Try using --dump-packets that should at least print the packets. Maybe add a timestamp there too. Have you changed your initial code? Maybe post your current code and the timing results?

TheSnoozer commented 5 years ago

Could you figure this out?

CamronW commented 5 years ago

Yeah, I gave up now aha. I spent too much time trying to figure it out. If anybody else has a solution it would be greatly appriciated!

TheSnoozer commented 5 years ago

Could you share your current version of the chunk data handling?

CamronW commented 5 years ago

Anyone else have any ideas on how to do this? I’ve tried to follow the Minecraft wiki’s transportation page on falling but I wasn’t able to get it to fall correctly. At this point I’d be willing to pay to get it solved

TheSnoozer commented 5 years ago

Could you perhaps share the current state of your pycraft-client (e.g. have you implemented some custom classes)? I'm willing to help....

CamronW commented 5 years ago

Hey,

So I have a pretty modified version at the moment, it would take a lot of work to try and post it all here. Currently what I'm doing is calling a fall() function every 5 seconds in an attempt to make it fall at least 1 block properly without getting stopped by servers anti-cheat.

Here's the function:

def fall():
    yVel = 0
    #for every tick
    #decrease y velocity by 0.08 blocks per tick
    #then multiply by 0.98
    for x in range(20):
        yVel -= 0.08
        yVel *= 0.98
        print(yVel, pos_look.y, pos_look.y + yVel)
        connection.write_packet(PositionAndLookPacket(
            x        = pos_look.x,
            feet_y  = pos_look.y + yVel,
            z        = pos_look.z,
            yaw    = pos_look.yaw,
            pitch    = pos_look.pitch,
            on_ground = False))
        time.sleep(0.05)

I'm using the numbers based off of other open source projects, as well as the Minecraft wiki's tranportation page (https://minecraft.gamepedia.com/Transportation#cite_ref-8) which states

Every tick (1⁄20 second), non-flying players and mobs have their vertical speed decremented (less upward motion, more downward motion) by 0.08 blocks per tick (1.6 m/s), then multiplied by 0.98.

An example of this being used is in Minecraft Console Client: https://github.com/ORelio/Minecraft-Console-Client/blob/ecf0114f626ac2ee6ae8ffbf2cceb69b7e0b1b09/MinecraftClient/Mapping/Movement.cs#L81

Another example, TwistedBot: https://github.com/lukleh/TwistedBot/blob/310509c037335845838e699f9f9d56af117e03c9/twistedbot/botentity.py#L340

I have no idea what I'm doing wrong anymore, in my head my solution should work.

CamronW commented 5 years ago

Any ideas @TheSnoozer

TheSnoozer commented 5 years ago

Yeah I got it to work, just need to polish the code over the weekend and then will post it here.

CamronW commented 5 years ago

Would you be able to post the unpolished version now should so I could take a look and try getting it working my end? I don't mind bad code

TheSnoozer commented 5 years ago

As requested - unpolished and a bit off the result I want. In general the idea is that I want to sent a 'move down' packet whenever the alt floats around in the air. This can happen when teleported (an indicator for being teleported is a PlayerPositionAndLookPacket packet), or when the block the alt is standing on is broken. Unfortunately the pycraft-library does not have any chunk information otherwise you could get the 'falling' implemented way better (e.g. you would know just by looking at the chunks and the current player position).

Anyways here is the draft, again right now a bit flaky - the thread sometimes stops in the middle of 'falling' causing the alt to stop falling.

```python #!/usr/bin/env python from __future__ import print_function import getpass import sys import re import time import threading from optparse import OptionParser from minecraft import authentication from minecraft.exceptions import YggdrasilError from minecraft.networking.connection import Connection from minecraft.networking.packets import Packet, clientbound, serverbound from minecraft.compat import input def get_options(): parser = OptionParser() parser.add_option("-u", "--username", dest="username", default=None, help="username to log in with") parser.add_option("-p", "--password", dest="password", default=None, help="password to log in with") parser.add_option("-s", "--server", dest="server", default=None, help="server host or host:port " "(enclose IPv6 addresses in square brackets)") parser.add_option("-o", "--offline", dest="offline", action="store_true", help="connect to a server in offline mode " "(no password required)") parser.add_option("-d", "--dump-packets", dest="dump_packets", action="store_true", help="print sent and received packets to standard error") (options, args) = parser.parse_args() if not options.username: options.username = input("Enter your username: ") if not options.password and not options.offline: options.password = getpass.getpass("Enter your password (leave " "blank for offline mode): ") options.offline = options.offline or (options.password == "") if not options.server: options.server = input("Enter server host or host:port " "(enclose IPv6 addresses in square brackets): ") # Try to split out port and address match = re.match(r"((?P[^\[\]:]+)|\[(?P[^\[\]]+)\])" r"(:(?P\d+))?$", options.server) if match is None: raise ValueError("Invalid server address: '%s'." % options.server) options.address = match.group("host") or match.group("addr") options.port = int(match.group("port") or 25565) return options class Player: entity_id = 0 x = 0 y = 0 z = 0 yaw = 0 pitch = 0 last_y_pos = 0 thread = None class SendDownPacket(threading.Thread): def __init__(self, connection): threading.Thread.__init__(self) self.shutdown_flag = threading.Event() self.connection = connection def run(self): print('Thread #%s started' % self.ident) while not self.shutdown_flag.is_set(): # send a packet every ~50ms time.sleep(0.05) Player.y -= 0.08 Player.y *= 0.98 position_response = serverbound.play.PositionAndLookPacket() position_response.on_ground = False position_response.x = Player.x position_response.feet_y = Player.y position_response.z = Player.z position_response.yaw = Player.yaw position_response.pitch = Player.pitch self.connection.write_packet(position_response) print('<-- %s' % position_response) print('last_y_pos=%s, player.y=%s' % (int(Player.last_y_pos) - 1, int(Player.y))) print('Thread #%s stopped' % self.ident) def main(): options = get_options() if options.offline: print("Connecting in offline mode...") connection = Connection( options.address, options.port, username=options.username) else: auth_token = authentication.AuthenticationToken() try: auth_token.authenticate(options.username, options.password) except YggdrasilError as e: print(e) sys.exit() print("Logged in as %s..." % auth_token.username) connection = Connection( options.address, options.port, auth_token=auth_token) if options.dump_packets: def print_incoming(packet): if type(packet) is Packet: # This is a direct instance of the base Packet type, meaning # that it is a packet of unknown type, so we do not print it. return print('--> %s' % packet, file=sys.stderr) def print_outgoing(packet): print('<-- %s' % packet, file=sys.stderr) connection.register_packet_listener( print_incoming, Packet, early=True) connection.register_packet_listener( print_outgoing, Packet, outgoing=True) def handle_join_game(join_game_packet): print('Connected.') Player.entity_id = join_game_packet.entity_id connection.register_packet_listener( handle_join_game, clientbound.play.JoinGamePacket) def handle_player_position_and_look(player_position_and_look_packet): # the y-coordinate is actually absolute feet position, normally Head Y - 1.62 player_position_and_look_packet.y -= 1.62 print('handle_player_position_and_look: last_y_pos=%s, player.y=%s, abs=%s' % ( Player.last_y_pos, Player.y, abs(Player.last_y_pos - player_position_and_look_packet.y))) if abs(Player.last_y_pos - player_position_and_look_packet.y) > 1.34: print("player_position_and_look_packet - Perhaps updated location?") if not Player.thread: Player.thread = SendDownPacket(connection) Player.thread.start() else: if Player.thread: Player.thread.shutdown_flag.set() Player.thread.join() Player.thread = None Player.last_y_pos = player_position_and_look_packet.y Player.x = player_position_and_look_packet.x Player.y = player_position_and_look_packet.y Player.z = player_position_and_look_packet.z Player.yaw = player_position_and_look_packet.yaw Player.pitch = player_position_and_look_packet.pitch print('--> %s' % player_position_and_look_packet) connection.register_packet_listener( handle_player_position_and_look, clientbound.play.PlayerPositionAndLookPacket) def handle_block_change_packet(block_change_packet): # print("block_change_packet - Perhaps updated location?") # print('--> %s' % block_change_packet) # TODO perhaps only update when the block is just below? if not Player.thread: Player.thread = SendDownPacket(connection) Player.thread.start() connection.register_packet_listener( handle_block_change_packet, clientbound.play.BlockChangePacket) def handle_multi_block_change_packet(multi_block_change_packet): # print("multi_block_change_packet - Perhaps updated location?") # print('--> %s' % multi_block_change_packet) # TODO perhaps only update when the block is just below? if not Player.thread: Player.thread = SendDownPacket(connection) Player.thread.start() connection.register_packet_listener( handle_multi_block_change_packet, clientbound.play.MultiBlockChangePacket) def print_chat(chat_packet): print("Message (%s): %s" % ( chat_packet.field_string('position'), chat_packet.json_data)) connection.register_packet_listener( print_chat, clientbound.play.ChatMessagePacket) connection.connect() while True: try: text = input() if text == "/respawn": print("respawning...") packet = serverbound.play.ClientStatusPacket() packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN connection.write_packet(packet) else: packet = serverbound.play.ChatPacket() packet.message = text connection.write_packet(packet) except KeyboardInterrupt: if Player.thread: Player.thread.shutdown_flag.set() Player.thread.join() Player.thread = None print("Bye!") sys.exit() if __name__ == "__main__": main() ```

PS: I should note that is this build open the start.py.

CamronW commented 5 years ago

I might be being stupid here, but on the untouched code you sent me it doesn't work?

https://i.gyazo.com/e2de400023ed52280933cf9017e01a59.mp4

Not sure if I'm doing anything wrong

CamronW commented 5 years ago

Did you end up creating a polished version @TheSnoozer

clragon commented 4 years ago

I am interested in this as well. Did you make a polished version you could post here, @TheSnoozer ?

HiddenKitten commented 4 years ago

from what I can see, that 'unpolished version' only attempts to fall when connecting, and stops trying to once it gets told by the server it's trying to fall into a block.

one anticheat I know of accounts for this and sends a position to the client to reset your velocity before you hit the ground, on login.

edit: scratch that I misread, but am also interested in this.

another edit, sorry: from what it looks like that code doesn't properly reset velocity on the server telling the client where they are, so falling will hit max velocity, then continue there even if it stops falling.

CamronW commented 4 years ago

Anyone else managed to get falling to work on pycraft?

Halo07 commented 4 years ago

Still nothing here?

MLG-fun commented 4 years ago

Maybe the PositionAndLookPacket is broken?