diff --git a/README.md b/README.md index c4c67f2..752b8bd 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,19 @@ client.on('text', (packet) => { // Listen for chat messages and echo them back. }) ``` +### Client example joining a Realm + +Example to connect to a Realm that the authenticating account is owner of or has been invited to: + +```js +const bedrock = require('bedrock-protocol') +const client = bedrock.createClient({ + realms: { + pickRealm: (realms) => realms[0] // Function which recieves an array of joined/owned Realms and must return a single Realm. Can be async + } +}) +``` + ### Server example *Can't connect locally on Windows? See the [faq](docs/FAQ.md)* diff --git a/docs/API.md b/docs/API.md index 6401d3b..c1da683 100644 --- a/docs/API.md +++ b/docs/API.md @@ -8,7 +8,7 @@ Returns a `Client` instance and connects to the server. | Parameter | Optionality | Description | | ----------- | ----------- |-| -| host | **Required** | host to connect to, for example `127.0.0.1`. | +| host | Conditional | Not required if `realms` is set. host to connect to, for example `127.0.0.1`. | | port | *optional* | port to connect to, default to **19132** | | version | *optional* | Version to connect as. If not specified, automatically match server version. | | offline | *optional* | default to **false**. Set this to true to disable Microsoft/Xbox auth. | @@ -22,6 +22,10 @@ Returns a `Client` instance and connects to the server. | useNativeRaknet | *optional* | Whether to use the C++ version of RakNet. Set to false to use JS. | | compressionLevel | *optional* | What zlib compression level to use, default to **7** | | batchingInterval | *optional* | How frequently, in milliseconds to flush and write the packet queue (default: 20ms) | +| realms | *optional* | An object which should contain one of the following properties: `realmId`, `realmInvite`, `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** | +| realms.realmId | *optional* | The id of the Realm to join. | +| realms.realmInvite | *optional* | The invite link/code of the Realm to join. | +| realms.pickRealm | *optional* | A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. | The following events are emitted by the client: * 'status' - When the client's login sequence status has changed @@ -134,6 +138,22 @@ Order of client event emissions: * 'join' - the client is ready to recieve game packets after successful server-client handshake * 'spawn' - emitted after the client has permission from the server to spawn +### Realm docs + +To make joining a Realm easier we've added an optional `realm` property to the client. It accepts the following options `realmId`, `realmInvite`, and `pickRealm`, supplying one of these will fetch host/port information for the specified Realm and then attempt to connect the bot. + - `realmId` - The id of the Realm to join. + - `realmInvite` - The invite code/link of the Realm to join. + - `pickRealm` - A function that will be called with a list of Realms to pick from. The function should return the Realm to join. + +```js +const bedrock = require('bedrock-protocol') +const client = bedrock.createClient({ + realms: { + pickRealm: (realms) => realms[0] // Function which recieves an array of joined/owned Realms and must return a single Realm. Can be async + } +}) +``` + ### Protocol docs For documentation on the protocol, and packets/fields see the [the protocol doc](https://minecraft-data.prismarine.js.org/?v=bedrock_1.18.0&d=protocol) (the emitted event names are the Packet types in lower case without the "packet_" prefix). More information on syntax can be found in CONTRIBUTING.md. When sending a packet, you must fill out all of the required fields. diff --git a/examples/client/realm.js b/examples/client/realm.js new file mode 100644 index 0000000..ac52282 --- /dev/null +++ b/examples/client/realm.js @@ -0,0 +1,13 @@ +/* eslint-disable */ +const bedrock = require('bedrock-protocol') +const client = bedrock.createClient({ + realms: { + // realmId: '1234567', // Connect the client to a Realm using the Realms ID + // realmInvite: 'https://realms.gg/AB1CD2EFA3B', // Connect the client to a Realm using the Realms invite URL or code + pickRealm: (realms) => realms.find(e => e.name === 'Realm Name') // Connect the client to a Realm using a function that returns a Realm + } +}) + +client.on('text', (packet) => { // Listen for chat messages + console.log('Received Text:', packet) +}) diff --git a/index.d.ts b/index.d.ts index b02d208..c828688 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,5 @@ import EventEmitter from "events" +import { Realm } from "prismarine-realms" declare module "bedrock-protocol" { type Version = '1.18.12' | '1.18.11' | '1.18.10' | '1.18.2' | '1.18.1' | '1.18.0' | '1.17.41' | '1.17.40' | '1.17.34' | '1.17.30' | '1.17.11' | '1.17.10' | '1.17.0' | '1.16.220' | '1.16.210' | '1.16.201' @@ -40,6 +41,8 @@ declare module "bedrock-protocol" { skipPing?: boolean // where to log connection information to (default to console.log) conLog? + // used to join a Realm instead of supplying a host/port + realms?: RealmsOptions } export interface ServerOptions extends Options { @@ -174,6 +177,12 @@ declare module "bedrock-protocol" { serverId: string } + export interface RealmsOptions { + realmId?: string + realmInvite?: string + pickRealm?: (realms: Realm[]) => Realm + } + export function createClient(options: ClientOptions): Client export function createServer(options: ServerOptions): Server diff --git a/package.json b/package.json index 12c09c9..b0ee475 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "minecraft-folder-path": "^1.2.0", "prismarine-auth": "^1.1.0", "prismarine-nbt": "^2.0.0", + "prismarine-realms": "^1.1.0", "protodef": "^1.14.0", "uuid-1345": "^1.0.2", "raknet-native": "^1.0.3" diff --git a/src/client/auth.js b/src/client/auth.js index 72a44cc..6e31da7 100644 --- a/src/client/auth.js +++ b/src/client/auth.js @@ -3,6 +3,7 @@ const { Authflow: PrismarineAuth, Titles } = require('prismarine-auth') const minecraftFolderPath = require('minecraft-folder-path') const debug = require('debug')('minecraft-protocol') const { uuidFrom } = require('../datatypes/util') +const { RealmAPI } = require('prismarine-realms') function validateOptions (options) { if (!options.profilesFolder) { @@ -16,7 +17,35 @@ function validateOptions (options) { async function realmAuthenticate (options) { validateOptions(options) - throw new Error('Not implemented') + + options.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) + + const api = RealmAPI.from(options.authflow, 'bedrock') + const realms = await api.getRealms() + + debug('realms', realms) + + if (!realms || !realms.length) throw Error('Couldn\'t find any Realms for the authenticated account') + + let realm + + if (options.realms.realmId) { + realm = realms.find(e => e.id === Number(options.realms.realmId)) + } else if (options.realms.realmInvite) { + realm = await api.getRealmFromInvite(options.realms.realmInvite) + } else if (options.realms.pickRealm) { + if (typeof options.realms.pickRealm !== 'function') throw Error('realms.pickRealm must be a function') + realm = await options.realms.pickRealm(realms) + } + + if (!realm) throw Error('Couldn\'t find a Realm to connect to. Authenticated account must be the owner or has been invited to the Realm.') + + const { host, port } = await realm.getAddress() + + debug('realms connection', { host, port }) + + options.host = host + options.port = port } /** diff --git a/src/createClient.js b/src/createClient.js index eec4412..3c19928 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -15,7 +15,7 @@ function createClient (options) { if (options.skipPing) { client.init() } else { - ping(options).then(ad => { + ping(client.options).then(ad => { const adVersion = ad.version?.split('.').slice(0, 3).join('.') // Only 3 version units client.options.version = options.version ?? (Options.Versions[adVersion] ? adVersion : Options.CURRENT_VERSION) client.conLog?.(`Connecting to server ${ad.motd} (${ad.name}), version ${ad.version}`, client.options.version !== ad.version ? ` (as ${client.options.version})` : '')