Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic cross-protocol support #331

Merged
merged 22 commits into from
Feb 2, 2016
Merged

Conversation

deathcap
Copy link
Member

Implements the dynamic part of #234 Cross version support

Ported from https://github.com/deathcap/node-minecraft-protocol-auto (which I can deprecate if this is merged)

To do:

  • Enhance createClient() to allow waiting/pausing so it can be reconfigured before connect -- src/client/setProtocol.js? (installs client.on('connect') listener)
  • Change the protocol negotiation to be supported with a createClient()-compatible API (vs createClientAsync)
  • Update to use minecraft-data protocolVersions
  • Move into src/client/ to act as plugin/middleware

@rom1504
Copy link
Member

rom1504 commented Jan 25, 2016

createClient doesn't create an "all ready" client: people will want to wait for the login packet for example.
So I think it would be okay to return a client synchronously, and do the ping and set protocol thingies inside of createClient "afterwards".
That would mean changes in Client, but it might be okay.
This would have the benefit of keeping just one createClient.

@rom1504
Copy link
Member

rom1504 commented Jan 25, 2016

Err probably not login, that's server side, but there are many "ready" events in mineflayer (spawn, received inventory,...), so people wait for one of these before writing anything to the client.
It's also important to be able to listen to client "error" event asap

@deathcap
Copy link
Member Author

I'd definitely like to maintain API compatibility with createClient, will have to investigate more to try to do this. The problem I was running into is that the client created by createClient would continue the protocol handshake even before I received the ping. Maybe need some way to "pause" the client object? Also to dynamically change the version and hostname after object construction.

@rom1504
Copy link
Member

rom1504 commented Jan 26, 2016

I think to do it cleanly you'd have to change createClient so it would
(optionally) "wait" (listen to the right event,...) for the ping pong to be
over before starting the handshake. It's createClient doing the state
changing, Client only provide the functionality to do so.
Also I think it would be possible that way to use the same socket for
pinging and for the rest. The vanilla client probably doesn't use the same
socket, but I think it makes sense.
Yeah it would be needed to change the version after the pong. It is kind of
already being done anyway since mc.ping default to 1.8.

On Tue, Jan 26, 2016, 03:33 deathcap [email protected] wrote:

I'd definitely like to maintain API compatibility with createClient, will
have to investigate more to try to do this. The problem I was running into
is that the client created by createClient would continue the protocol
handshake even before I received the ping. Maybe need some way to "pause"
the client object? Also to dynamically change the version and hostname
after object construction.


Reply to this email directly or view it on GitHub
#331 (comment)
.

@deathcap
Copy link
Member Author

Yeah it would be needed to change the version after the pong. It is kind of
already being done anyway since mc.ping default to 1.8.

I think that's problematic for other reasons, imho the 0xfe01 ping #332 is superior, as it does not require a status handshake, nor a protocol version (and works on all 1.4.4+)

I think to do it cleanly you'd have to change createClient so it would
(optionally) "wait" (listen to the right event,...) for the ping pong to be
over before starting the handshake. It's createClient doing the state
changing, Client only provide the functionality to do so.

Notes for testing this scenario:

edit src/createServer.js to comment out client.once('ping_start', onPing);

node examples/server/server.js

NODE_DEBUG=mc-proto node examples/client_auto/client_auto.js localhost 25565

client should hang (hits #329 Missing ping timeout callback when server is running but not responding to pings) or timeout:

node-minecraft-protocol $ NODE_DEBUG=mc-proto node examples/client_auto/client_auto.js localhost 25565
MC-PROTO: 54954 creating client
MC-PROTO: 54954 pinging localhost
MC-PROTO: 54954 writing packet handshaking.set_protocol
MC-PROTO: 54954 { protocolVersion: 47,
  serverHost: 'localhost',
  serverPort: 25565,
  nextState: 1 }
MC-PROTO: 54954 writing packet status.ping_start
MC-PROTO: 54954 {}

then edit src/createClientAuto.js (from this PR), move the createClient() call outside of the ping callback directly into createClientAsync(), and rerun client_auto.js

now, the client will despite not receiving a ping (callback not executed), it will still begin to connect:

node-minecraft-protocol $ NODE_DEBUG=mc-proto node examples/client_auto/client_auto.js localhost 25565
MC-PROTO: 54970 creating client
MC-PROTO: 54970 pinging localhost
MC-PROTO: 54970 writing packet handshaking.set_protocol
MC-PROTO: 54970 { protocolVersion: 47,
  serverHost: 'localhost',
  serverPort: 25565,
  nextState: 2 }
MC-PROTO: 54970 writing packet login.login_start
MC-PROTO: 54970 { username: 'echo' }
MC-PROTO: 54970 writing packet handshaking.set_protocol
MC-PROTO: 54970 { protocolVersion: 47,
  serverHost: 'localhost',
  serverPort: 25565,
  nextState: 1 }
MC-PROTO: 54970 writing packet status.ping_start
MC-PROTO: 54970 {}
MC-PROTO: 54970 read packet login.compress
MC-PROTO: 54970 { threshold: 256 }
MC-PROTO: 54970 read packet login.success
MC-PROTO: 54970 { uuid: 'c39fa4c6-85a3-3582-8167-db3805dbe851',
  username: 'echo' }
MC-PROTO: 54970 read packet play.chat
MC-PROTO: 54970 { message: '{"translate":"chat.type.text","with":["Server","echo joined the game."]}',
  position: 0 }
MC-PROTO: 54970 read packet play.login
MC-PROTO: 54970 { entityId: 1,
  gameMode: 1,
  dimension: 0,
  difficulty: 2,
  maxPlayers: 127,
  levelType: 'default',
  reducedDebugInfo: false }
MC-PROTO: 54970 read packet play.position
MC-PROTO: 54970 { x: 0, y: 256, z: 0, yaw: 0, pitch: 0, flags: 0 }
MC-PROTO: 54970 read packet play.keep_alive
MC-PROTO: 54970 { keepAliveId: 2138214823 }
MC-PROTO: 54970 writing packet play.keep_alive
MC-PROTO: 54970 { keepAliveId: 2138214823 }
MC-PROTO: 54970 read packet play.keep_alive
MC-PROTO: 54970 { keepAliveId: 648251752 }
MC-PROTO: 54970 writing packet play.keep_alive
MC-PROTO: 54970 { keepAliveId: 648251752 }
MC-PROTO: 54970 read packet play.keep_alive
MC-PROTO: 54970 { keepAliveId: 1917937053 }
MC-PROTO: 54970 writing packet play.keep_alive
MC-PROTO: 54970 { keepAliveId: 1917937053 }


Now that the client is more modular, it's easier to see what's going on, need to test to confirm but I think this is where it is unexpectedly (or undesirably) executing from:

// src/client/setProtocol.js
ar states = require("../states");

module.exports = function(client, options) {
  client.on('connect', onConnect);

  function onConnect() {
    client.write('set_protocol', {
      protocolVersion: options.protocolVersion,
      serverHost: options.host,
      serverPort: options.port,
      nextState: 2
    });

setProtocol sends the set_protocol packet as soon as the socket connects.

(and src/client/caseCorrect.js calls client options.connect(), which is set from src/client/tcp_dns.js to call client.setSocket())

@deathcap
Copy link
Member Author

caseCorrecting and tcp_dns (SRV lookup and TCP initiation) can occur beforehand, but sending set_protocol should be delayed until the client knows (via ping) the protocolVersion.

@deathcap
Copy link
Member Author

Listen for another event in setProtocol before sending the packet? 'connect' is going to be received when the TCP socket it connected, whether we are ready for it or not. Additionally block on another event (optionally), something like 'connect_allowed'?

…c/client/setProtocol proceeds

Intended to allow clients to configure the client object before any data
is sent, specifically, before src/client/setProtocol sends the
set_protocol packet.
@deathcap
Copy link
Member Author

Also I think it would be possible that way to use the same socket for
pinging and for the rest. The vanilla client probably doesn't use the same
socket, but I think it makes sense.

The server immediately closes the socket after sending the ping response, so unfortunately I don't think reusing it is possible.

This allows client plugins to change the options on the client object,
and have it reflected in other plugins. Previously, had to rely on the
options being the same object, and hold a reference to it. Now it is
always accessible as `client.options`.
@deathcap
Copy link
Member Author

Converted to use "sync" API ok, started to move into createClient() via a src/client/autoVersion.js plugin. Having some difficulty re-versioning the client object after it is instantiated. The constructor calls setHandshake() and sets .version to the major version, so I do this too in autoVersion.js after the version is ready, and then emit connect_allowed to uncork setProtocol, it sends the set_protocol packet but then the program just exits:

node-minecraft-protocol $ NODE_DEBUG=mc-proto node examples/client_auto/client_auto.js localhost 9999
MC-PROTO: 7249 creating client
MC-PROTO: 7249 pinging localhost
connected
MC-PROTO: 7249 writing packet handshaking.set_protocol
MC-PROTO: 7249 { protocolVersion: 47,
  serverHost: 'localhost',
  serverPort: 9999,
  nextState: 1 }
MC-PROTO: 7249 writing packet status.ping_start
MC-PROTO: 7249 {}
MC-PROTO: 7249 read packet status.server_info
MC-PROTO: 7249 { response: '{"description":{"text":"A Minecraft Server"},"players":{"max":20,"online":0},"version":{"name":"15w40b","protocol":76}}' }
MC-PROTO: 7249 writing packet status.ping
MC-PROTO: 7249 { time: [ 0, 0 ] }
MC-PROTO: 7249 read packet status.ping
MC-PROTO: 7249 { time: [ 0, 0 ] }
MC-PROTO: 7249 ping response { description: { text: 'A Minecraft Server' },
  players: { max: 20, online: 0 },
  version: { name: '15w40b', protocol: 76 },
  latency: 1 }
MC-PROTO: 7249 Server description: { text: 'A Minecraft Server' }
MC-PROTO: 7249 Server version: 15w40b, protocol: 76
{ minecraftVersion: '15w40b',
  version: 76,
  usesNetty: true,
  majorVersion: '1.9' }
MC-PROTO: 7249 writing packet handshaking.set_protocol
MC-PROTO: 7249 { protocolVersion: 76,
  serverHost: 'localhost',
  serverPort: 9999,
  nextState: 2 }
MC-PROTO: 7249 writing packet login.login_start
MC-PROTO: 7249 { username: 'echo' }

@deathcap
Copy link
Member Author

Looks like the ping connection is somehow interfering with the subsequent normal connection. connected is logged right after "pinging localhost", when receiving the client connect event, but not for the re-connect with the new version. The program ends because the client receives the end event.


The logged connected message is actually from the normal connection, the TCP socket connects almost immediately, but is held without reply until the ping is received. client_echo logs for comparison:

node-minecraft-protocol $ NODE_DEBUG=mc-proto node examples/client_echo/client_echo.js localhost 18900
setProtocol onConnect
MC-PROTO: 11592 writing packet handshaking.set_protocol
MC-PROTO: 11592 { protocolVersion: 47,
  serverHost: 'localhost',
  serverPort: 18900,
  nextState: 2 }
MC-PROTO: 11592 writing packet login.login_start
MC-PROTO: 11592 { username: 'echo' }
connected
MC-PROTO: 11592 read packet login.compress
MC-PROTO: 11592 { threshold: 256 }
MC-PROTO: 11592 read packet login.success
MC-PROTO: 11592 { uuid: 'c39fa4c6-85a3-3582-8167-db3805dbe851',
  username: 'echo' }
MC-PROTO: 11592 read packet play.login
MC-PROTO: 11592 { entityId: 364,
  gameMode: 0,
  dimension: 0,
  difficulty: 1,
  maxPlayers: 20,
  levelType: 'default',
  reducedDebugInfo: false }
MC-PROTO: 11592 read packet play.custom_payload
MC-PROTO: 11592 { channel: 'MC|Brand', data: <Buffer 07 76 61 6e 69 6c 6c 61> }
MC-PROTO: 11592 read packet play.difficulty
MC-PROTO: 11592 { difficulty: 1 }
MC-PROTO: 11592 read packet play.spawn_position
MC-PROTO: 11592 { location: { x: -8, y: 64, z: -196 } }

Problem with the serializer? Packet capture shows the handshaking.set_protocol packet is never sent on the 2nd connection, only the login.login_start packet:

00000000  06                                               .
00000001  00 04 65 63 68 6f                                ..echo

even though it was logged as being written in:

setting serializer for state=handshaking, version=1.9
MC-PROTO: 12311 writing packet handshaking.set_protocol
MC-PROTO: 12311 { protocolVersion: 76,
  serverHost: 'localhost',
  serverPort: 9999,
  nextState: 2 }
setting serializer for state=login, version=1.9
MC-PROTO: 12311 writing packet login.login_start
MC-PROTO: 12311 { username: 'echo' }
ended

@deathcap
Copy link
Member Author

Updating the client state using client.state = states.HANDSHAKING instead of client.setSerializer(states.HANDSHAKING) resolves the above issue, now able to connect with the auto-negotiated protocol.

(why I was confused: the src/client.js constructor calls this.setSerializer, not this.state=..., tried to mimic that behavior to re-initialize the version, but set state(newProperty) has a setter to perform important useful tasks of piping/unpiping the serializers).

@deathcap
Copy link
Member Author

deathcap commented Feb 1, 2016

Failing tests in requiring minecraft-data: PrismarineJS/node-minecraft-data#16

Attempted to use latest node-minecraft-data from git, but still fails, I think CircleCI is caching the node_modules. Will try a clean CircleCI setup.

deathcap added a commit to deathcap/node-minecraft-protocols that referenced this pull request Feb 1, 2016
@deathcap deathcap force-pushed the dynxver branch 2 times, most recently from 20564a4 to 80fe58f Compare February 1, 2016 05:26
@@ -1,7 +1,8 @@
var yggdrasil = require('yggdrasil')({});
var UUID = require('uuid-1345');

module.exports = function(client, options) {
module.exports = function(client) {
var options = client.options;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why client.options ? The client, options pattern is consistent with what mineflayer is doing, and it seems to makes sense. (It also allows destructuring : for example https://github.com/PrismarineJS/flying-squid/blob/master/src/lib/plugins/world.js#L10 )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I saw your commit message "This allows client plugins to change the options on the client object,
and have it reflected in other plugins. Previously, had to rely on the
options being the same object, and hold a reference to it. Now it is
always accessible as client.options."

It sounds like it assume some order of execution between plugins. Which plugins is accessing the modified property ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah that kind of thing https://github.com/PrismarineJS/node-minecraft-protocol/pull/331/files#diff-9053b72ce1984293dcb954a0ccc89097R9.
Couldn't it be put in a client.wait_connect ?
But the problem of relying on the order of execution of plugins remains.

@rom1504
Copy link
Member

rom1504 commented Feb 1, 2016

I made some comments

(I guess it's probably necessary for some plugins to depend on other, but let's probably keep it implicit for now)

@roblabla
Copy link
Member

roblabla commented Feb 1, 2016

I am very, very, very much against the client.options pattern. Ideally, I should be able to do

var client = new Client();
tcpConnect(client, { opts for tcpConnect });
forgeHandshake(client, { opts for forgeHandshake });
play(client, { opts for play });
etc...

What plugin needs modifying the options ? I believe that if any plugin requires this, it should instead wrap the original plugin in some way.

@deathcap
Copy link
Member Author

deathcap commented Feb 1, 2016

Updated to data 0.19.1, removing need for git clone post-action

Plugins currently modifying options:

  • autoVersion: sets options.wait_connect, used by setProtocol plugin
  • caseCorrect: sets accessToken and haveCredentials
  • tcp_dns: sets port and host to defaults if not set, and sets connect to callback
  • forgeHandshake: sets tagHost

I suppose these 'options' could be moved onto the client object, since they're not really options but data passed between plugins (which can initially come from options, but only as a default). tagHost could use the system described in #336 (comment)


// Get the minecraft-data version string for a protocol version
// TODO: add to node-minecraft-data index (protocol to newest release, if multiple)
function protocolVersion2MinecraftVersion(n) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, missed that, will use postNettyVersionsByProtocolVersion

@deathcap
Copy link
Member Author

deathcap commented Feb 2, 2016

Alright I've restored the options parameters to each of the client plugin parameters. This gets us closer to separate independent options per plugin, but a few plugins still modify their options making them interdependent.

@deathcap
Copy link
Member Author

deathcap commented Feb 2, 2016

Moved wait_connect from options to the client object, but moving all the other plugin-mutable options to client is a much bigger change, would prefer it as a separate PR (poses some non-obvious questions, e.g. how to handle the version parameter in the options vs the client object)

@rom1504
Copy link
Member

rom1504 commented Feb 2, 2016

Ok good enough. We can track the remaining things to do here in #338 and #234

rom1504 added a commit that referenced this pull request Feb 2, 2016
Add dynamic cross-protocol support
@rom1504 rom1504 merged commit 0ba187d into PrismarineJS:master Feb 2, 2016
@rom1504 rom1504 mentioned this pull request Feb 2, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants