diff --git a/data/new/proto.yml b/data/new/proto.yml index 0965d3d..0a3bd85 100644 --- a/data/new/proto.yml +++ b/data/new/proto.yml @@ -1,9 +1,9 @@ # Created from MiNET docs string: ["pstring",{"countType":"varint"}] ByteArray: ["buffer",{"countType":"varint"}] +SignedByteArray: ["buffer",{"countType":"zigzag32"}] LittleString: ["pstring",{"countType":"li32"}] varint32: varint -varint64: varint bool: native zigzag32: native zigzag64: native @@ -166,6 +166,7 @@ packet_text: _: type? if chat or whisper or announcement: source_name: string + message: string if raw or tip or system or json_whisper or json: message: string if translation or popup or jukebox_popup: @@ -718,7 +719,7 @@ packet_mob_equipment: item: Item slot: u8 selected_slot: u8 - windows_id: u8 + windows_id: WindowID packet_mob_armor_equipment: !id: 0x20 @@ -865,46 +866,94 @@ packet_respawn: state: u8 runtime_entity_id: varint +# ContainerOpen is sent by the server to open a container client-side. This container must be physically +# present in the world, for the packet to have any effect. Unlike Java Edition, Bedrock Edition requires that +# chests for example must be present and in range to open its inventory. packet_container_open: !id: 0x2e !bound: client + # WindowID is the ID representing the window that is being opened. It may be used later to close the + # container using a ContainerClose packet. window_id: u8 - type: u8 + # ContainerType is the type ID of the container that is being opened when opening the container at the + # position of the packet. It depends on the block/entity, and could, for example, be the window type of + # a chest or a hopper, but also a horse inventory. + window_type: WindowType + # ContainerPosition is the position of the container opened. The position must point to a block entity + # that actually has a container. If that is not the case, the window will not be opened and the packet + # will be ignored, if a valid ContainerEntityUniqueID has not also been provided. coordinates: BlockCoordinates + # ContainerEntityUniqueID is the unique ID of the entity container that was opened. It is only used if + # the ContainerType is one that points to an entity, for example a horse. runtime_entity_id: zigzag64 +# ContainerClose is sent by the server to close a container the player currently has opened, which was opened +# using the ContainerOpen packet, or by the client to tell the server it closed a particular container, such +# as the crafting grid. packet_container_close: !id: 0x2f !bound: both + # WindowID is the ID representing the window of the container that should be closed. It must be equal to + # the one sent in the ContainerOpen packet to close the designated window. window_id: u8 + # ServerSide determines whether or not the container was force-closed by the server. If this value is + # not set correctly, the client may ignore the packet and respond with a PacketViolationWarning. server: bool +# PlayerHotBar is sent by the server to the client. It used to be used to link hot bar slots of the player to +# actual slots in the inventory, but as of 1.2, this was changed and hot bar slots are no longer a free +# floating part of the inventory. +# Since 1.2, the packet has been re-purposed, but its new functionality is not clear. packet_player_hotbar: !id: 0x30 !bound: both selected_slot: varint - window_id: u8 + window_id: WindowID select_slot: bool +# InventoryContent is sent by the server to update the full content of a particular inventory. It is usually +# sent for the main inventory of the player, but also works for other inventories that are currently opened +# by the player. packet_inventory_content: !id: 0x31 !bound: both + # WindowID is the ID that identifies one of the windows that the client currently has opened, or one of + # the consistent windows such as the main inventory. inventory_id: varint + # Content is the new content of the inventory. The length of this slice must be equal to the full size of + # the inventory window updated. input: ItemStacks +# InventorySlot is sent by the server to update a single slot in one of the inventory windows that the client +# currently has opened. Usually this is the main inventory, but it may also be the off hand or, for example, +# a chest inventory. packet_inventory_slot: !id: 0x32 !bound: both - inventory_id: varint + # WindowID is the ID of the window that the packet modifies. It must point to one of the windows that the + # client currently has opened. + window_id: varint + # Slot is the index of the slot that the packet modifies. The new item will be set to the slot at this + # index. slot: varint + # NewItem is the item to be put in the slot at Slot. It will overwrite any item that may currently + # be present in that slot. uniqueid: zigzag32 item: Item +# ContainerSetData is sent by the server to update specific data of a single container, meaning a block such +# as a furnace or a brewing stand. This data is usually used by the client to display certain features +# client-side. packet_container_set_data: !id: 0x33 !bound: client - window_id: u8 + # WindowID is the ID of the window that should have its data set. The player must have a window open with + # the window ID passed, or nothing will happen. + window_id: WindowID + # Key is the key of the property. It is one of the constants that can be found above. Multiple properties + # share the same key, but the functionality depends on the type of the container that the data is set to. property: zigzag32 + # Value is the value of the property. Its use differs per property. value: zigzag32 packet_crafting_data: @@ -915,20 +964,33 @@ packet_crafting_data: potion_container_recipes: PotionContainerChangeRecipes is_clean: bool +# CraftingEvent is sent by the client when it crafts a particular item. Note that this packet may be fully +# ignored, as the InventoryTransaction packet provides all the information required. packet_crafting_event: !id: 0x35 !bound: both - window_id: u8 - recipe_type: zigzag32 + # WindowID is the ID representing the window that the player crafted in. + window_id: WindowID + # CraftingType is a type that indicates the way the crafting was done, for example if a crafting table + # was used. + recipe_type: zigzag32 => + 0: inventory + 1: crafting + 2: workbench + # RecipeUUID is the UUID of the recipe that was crafted. It points to the UUID of the recipe that was + # sent earlier in the CraftingData packet. recipe_id: uuid - input: ItemStacks - result: ItemStacks + # Input is a list of items that the player put into the recipe so that it could create the Output items. + # These items are consumed in the process. + input: Item[]varint + # Output is a list of items that were obtained as a result of crafting the recipe. + result: Item[]varint + packet_gui_data_pick_item: !id: 0x36 !bound: client - # AdventureSettings is sent by the server to update game-play related features, in particular permissions to # access these features for the client. It includes allowing the player to fly, build and mine, and attack # entities. Most of these flags should be checked server-side instead of using this packet only. @@ -1085,7 +1147,7 @@ packet_event: !id: 0x41 !bound: client runtime_id: varint64 - event_type: varint => + event_type: zigzag32 => 0: achievement_awarded 1: entity_interact 2: portal_built @@ -1105,6 +1167,7 @@ packet_event: 16: composter_block_used 17: bell_block_used use_player_id: u8 + event_data: restBuffer # Unknown data, TODO: add packet_spawn_experience_orb: !id: 0x42 @@ -1821,13 +1884,36 @@ packet_level_sound_event: is_baby_mob: bool is_global: bool +# LevelEventGeneric is sent by the server to send a 'generic' level event to the client. This packet sends an +# NBT serialised object and may for that reason be used for any event holding additional data. packet_level_event_generic: !id: 0x7c !bound: client + # EventID is a unique identifier that identifies the event called. The data that follows has fields in + # the NBT depending on what event it is. + event_id: varint + # SerialisedEventData is a network little endian serialised object of event data, with fields that vary + # depending on EventID. + # Unlike many other NBT structures, this data is not actually in a compound but just loosely floating + # NBT tags. To decode using the nbt package, you would need to append 0x0a00 at the start (compound id + # and name length) and add 0x00 at the end, to manually wrap it in a compound. Likewise, you would have + # to remove these bytes when encoding. + nbt: nbtLoop +# LecternUpdate is sent by the client to update the server on which page was opened in a book on a lectern, +# or if the book should be removed from it. packet_lectern_update: !id: 0x7d !bound: client + # Page is the page number in the book that was opened by the player on the lectern. + page: u8 + # PageCount is the number of pages that the book opened in the lectern has. + page_count: u8 + # Position is the position of the lectern that was updated. If no lectern is at the block position, + # the packet should be ignored. + position: vec3i + # DropBook specifies if the book currently set on display in the lectern should be dropped server-side. + drop_book: bool packet_video_stream_connect: !id: 0x7e @@ -1890,11 +1976,15 @@ packet_update_block_properties: packet_client_cache_blob_status: !id: 0x87 !bound: client + # The number of MISSes in this packet + misses: varint + # The number of HITs in this packet + haves: varint # A list of blob hashes that the client does not have a blob available for. The server # should send the blobs matching these hashes as soon as possible. - missing: lu64[]varint + missing: lu64[]$misses # A list of hashes that the client does have a cached blob for. Server doesn't need to send. - have: lu64[]varint + have: lu64[]$haves # ClientCacheMissResponse is part of the blob cache protocol. It is sent by the server in response to a # ClientCacheBlobStatus packet and contains the blob data of all blobs that the client acknowledged not to @@ -2051,7 +2141,7 @@ packet_player_auth_input: delta: vec3f InputFlag: [ "bitflags", { - "type": "varint64", + "type": "varint", "flags": { "ascend": 0b1, "descend": 0b10, diff --git a/data/new/types.yaml b/data/new/types.yaml index 9be85ae..adf7b3b 100644 --- a/data/new/types.yaml +++ b/data/new/types.yaml @@ -64,7 +64,7 @@ Blob: hash: lu64 # Payload is the data of the blob. When sent, the client will associate the Hash of the blob with the # Payload in it. - payload: string + payload: ByteArray BlockPalette: []varint name: string @@ -586,52 +586,121 @@ StackRequestSlotInfo: # ItemStackRequests: []varint + # RequestID is a unique ID for the request. This ID is used by the server to send a response for this + # specific request in the ItemStackResponse packet. request_id: zigzag32 actions: []varint type_id: u8 => - '0': 'TAKE' - '1': 'PLACE' - '2': 'SWAP' - '3': 'DROP' - '4': 'DESTROY' - '5': 'CRAFTING_CONSUME_INPUT' - '6': 'create' - '7': 'LAB_TABLE_COMBINE' - '8': 'BEACON_PAYMENT' - '9': 'CRAFTING_RECIPE' - '10': 'CRAFTING_RECIPE_AUTO' #recipe book? - '11': 'CREATIVE_CREATE' - '12': 'CRAFTING_NON_IMPLEMENTED_DEPRECATED' #anvils aren't fully implemented yet - '13': 'CRAFTING_RESULTS_DEPRECATED' #no idea what this is for + # TakeStackRequestAction is sent by the client to the server to take x amount of items from one slot in a + # container to the cursor. + 0: take + # PlaceStackRequestAction is sent by the client to the server to place x amount of items from one slot into + # another slot, such as when shift clicking an item in the inventory to move it around or when moving an item + # in the cursor into a slot. + 1: place + # SwapStackRequestAction is sent by the client to swap the item in its cursor with an item present in another + # container. The two item stacks swap places. + 2: swap + # DropStackRequestAction is sent by the client when it drops an item out of the inventory when it has its + # inventory opened. This action is not sent when a player drops an item out of the hotbar using the Q button + # (or the equivalent on mobile). The InventoryTransaction packet is still used for that action, regardless of + # whether the item stack network IDs are used or not. + 3: drop + # DestroyStackRequestAction is sent by the client when it destroys an item in creative mode by moving it + # back into the creative inventory. + 4: destroy + # ConsumeStackRequestAction is sent by the client when it uses an item to craft another item. The original + # item is 'consumed'. + 5: consume + # CreateStackRequestAction is sent by the client when an item is created through being used as part of a + # recipe. For example, when milk is used to craft a cake, the buckets are leftover. The buckets are moved to + # the slot sent by the client here. + # Note that before this is sent, an action for consuming all items in the crafting table/grid is sent. Items + # that are not fully consumed when used for a recipe should not be destroyed there, but instead, should be + # turned into their respective resulting items. + 6: create + # LabTableCombineStackRequestAction is sent by the client when it uses a lab table to combine item stacks. + 7: lab_table_combine + # BeaconPaymentStackRequestAction is sent by the client when it submits an item to enable effects from a + # beacon. These items will have been moved into the beacon item slot in advance. + 8: beacon_payment + # CraftRecipeStackRequestAction is sent by the client the moment it begins crafting an item. This is the + # first action sent, before the Consume and Create item stack request actions. + # This action is also sent when an item is enchanted. Enchanting should be treated mostly the same way as + # crafting, where the old item is consumed. + 9: craft_recipe + # AutoCraftRecipeStackRequestAction is sent by the client similarly to the CraftRecipeStackRequestAction. The + # only difference is that the recipe is automatically created and crafted by shift clicking the recipe book. + 10: craft_recipe_auto #recipe book? + # CraftCreativeStackRequestAction is sent by the client when it takes an item out fo the creative inventory. + # The item is thus not really crafted, but instantly created. + 11: craft_creative + # CraftRecipeOptionalStackRequestAction is sent when using an anvil. When this action is sent, the + # CustomNames field in the respective stack request is non-empty and contains the name of the item created + # using the anvil. + 12: optional + # CraftNonImplementedStackRequestAction is an action sent for inventory actions that aren't yet implemented + # in the new system. These include, for example, anvils. + 13: non_implemented #anvils aren't fully implemented yet + # CraftResultsDeprecatedStackRequestAction is an additional, deprecated packet sent by the client after + # crafting. It holds the final results and the amount of times the recipe was crafted. It shouldn't be used. + # This action is also sent when an item is enchanted. Enchanting should be treated mostly the same way as + # crafting, where the old item is consumed. + 14: results_deprecated _: type_id ? - if TAKE or PLACE: + if take or place: count: u8 source: StackRequestSlotInfo destination: StackRequestSlotInfo - if SWAP: + if swap: + # Source and Destination point to the source slot from which Count of the item stack were taken and the + # destination slot to which this item was moved. source: StackRequestSlotInfo destination: StackRequestSlotInfo - if DROP: + if drop: + # Count is the count of the item in the source slot that was taken towards the destination slot. count: u8 + # Source is the source slot from which items were dropped to the ground. source: StackRequestSlotInfo + # Randomly seems to be set to false in most cases. I'm not entirely sure what this does, but this is what + # vanilla calls this field. randomly: bool - if DESTROY or CRAFTING_CONSUME_INPUT: + if destroy or consume: + # Count is the count of the item in the source slot that was destroyed. count: u8 + # Source is the source slot from which items came that were destroyed by moving them into the creative + # inventory. source: StackRequestSlotInfo if create: + # ResultsSlot is the slot in the inventory in which the results of the crafting ingredients are to be + # placed. result_slot_id: u8 - if BEACON_PAYMENT: - primary_effect: varint - secondary_effect: varint - if CRAFTING_RECIPE or CRAFTING_RECIPE_AUTO: - recipe_network_id: varint32 - if CREATIVE_CREATE: + if beacon_payment: + # PrimaryEffect and SecondaryEffect are the effects that were selected from the beacon. + primary_effect: zigzag32 + secondary_effect: zigzag32 + if craft_recipe or craft_recipe_auto: + # RecipeNetworkID is the network ID of the recipe that is about to be crafted. This network ID matches + # one of the recipes sent in the CraftingData packet, where each of the recipes have a RecipeNetworkID as + # of 1.16. + recipe_network_id: varint + if craft_creative: + # CreativeItemNetworkID is the network ID of the creative item that is being created. This is one of the + # creative item network IDs sent in the CreativeContent packet. creative_item_network_id: varint32 - if CRAFTING_NON_IMPLEMENTED_DEPRECATED: void - if CRAFTING_RESULTS_DEPRECATED: - result_items: ItemStacks + if optional: + # For the cartography table, if a certain MULTI recipe is being called, this points to the network ID that was assigned. + recipe_network_id: varint + # Most likely the index in the request's filter strings that this action is using + filtered_string_index: li32 + if non_implemented: void + if results_deprecated: + result_items: Item[]varint times_crafted: u8 - + # CustomNames is a list of custom names involved in the request. This is typically filled with one string + # when an anvil is used. + # * Used for the server to determine which strings should be filtered. Used in anvils to verify a renamed item. + custom_names: string[]varint ItemStackResponses: []varint result: u8 @@ -683,6 +752,36 @@ CommandOrigin: if dev_console or test: player_entity_id: zigzag64 +# Some arbitrary definitions from CBMC, Window IDs are normally +# unique + sequential +WindowID: i8 => + -100: drop_contents + -24: beacon + -23: trading_output + -22: trading_use_inputs + -21: trading_input_2 + -20: trading_input_1 + -17: enchant_output + -16: enchant_material + -15: enchant_input + -13: anvil_output + -12: anvil_result + -11: anvil_material + -10: container_input + -5: crafting_use_ingredient + -4: crafting_result + -3: crafting_remove_ingredient + -2: crafting_add_ingredient + -1: none + 0: inventory + 1: first + 100: last + 119: offhand + 120: armor + 121: creative + 122: hotbar + 123: fixed_inventory + 124: ui WindowType: u8 => 0: container diff --git a/data/newproto.json b/data/newproto.json index bbce33d..4c90341 100644 --- a/data/newproto.json +++ b/data/newproto.json @@ -1,7 +1,6 @@ { "types": { "varint32": "varint", - "varint64": "varint", "bool": "native", "zigzag32": "native", "zigzag64": "native", @@ -209,7 +208,7 @@ }, { "name": "payload", - "type": "string" + "type": "ByteArray" } ] ], @@ -2037,20 +2036,21 @@ { "type": "u8", "mappings": { - "0": "TAKE", - "1": "PLACE", - "2": "SWAP", - "3": "DROP", - "4": "DESTROY", - "5": "CRAFTING_CONSUME_INPUT", + "0": "take", + "1": "place", + "2": "swap", + "3": "drop", + "4": "destroy", + "5": "consume", "6": "create", - "7": "LAB_TABLE_COMBINE", - "8": "BEACON_PAYMENT", - "9": "CRAFTING_RECIPE", - "10": "CRAFTING_RECIPE_AUTO", - "11": "CREATIVE_CREATE", - "12": "CRAFTING_NON_IMPLEMENTED_DEPRECATED", - "13": "CRAFTING_RESULTS_DEPRECATED" + "7": "lab_table_combine", + "8": "beacon_payment", + "9": "craft_recipe", + "10": "craft_recipe_auto", + "11": "craft_creative", + "12": "optional", + "13": "non_implemented", + "14": "results_deprecated" } } ] @@ -2062,7 +2062,7 @@ { "compareTo": "type_id", "fields": { - "TAKE": [ + "take": [ "container", [ { @@ -2079,7 +2079,7 @@ } ] ], - "PLACE": [ + "place": [ "container", [ { @@ -2096,7 +2096,7 @@ } ] ], - "SWAP": [ + "swap": [ "container", [ { @@ -2109,7 +2109,7 @@ } ] ], - "DROP": [ + "drop": [ "container", [ { @@ -2126,7 +2126,7 @@ } ] ], - "DESTROY": [ + "destroy": [ "container", [ { @@ -2139,7 +2139,7 @@ } ] ], - "CRAFTING_CONSUME_INPUT": [ + "consume": [ "container", [ { @@ -2161,38 +2161,38 @@ } ] ], - "BEACON_PAYMENT": [ + "beacon_payment": [ "container", [ { "name": "primary_effect", - "type": "varint" + "type": "zigzag32" }, { "name": "secondary_effect", + "type": "zigzag32" + } + ] + ], + "craft_recipe": [ + "container", + [ + { + "name": "recipe_network_id", "type": "varint" } ] ], - "CRAFTING_RECIPE": [ + "craft_recipe_auto": [ "container", [ { "name": "recipe_network_id", - "type": "varint32" + "type": "varint" } ] ], - "CRAFTING_RECIPE_AUTO": [ - "container", - [ - { - "name": "recipe_network_id", - "type": "varint32" - } - ] - ], - "CREATIVE_CREATE": [ + "craft_creative": [ "container", [ { @@ -2201,13 +2201,32 @@ } ] ], - "CRAFTING_NON_IMPLEMENTED_DEPRECATED": "void", - "CRAFTING_RESULTS_DEPRECATED": [ + "optional": [ + "container", + [ + { + "name": "recipe_network_id", + "type": "varint" + }, + { + "name": "filtered_string_index", + "type": "li32" + } + ] + ], + "non_implemented": "void", + "results_deprecated": [ "container", [ { "name": "result_items", - "type": "ItemStacks" + "type": [ + "array", + { + "countType": "varint", + "type": "Item" + } + ] }, { "name": "times_crafted", @@ -2224,6 +2243,16 @@ ] } ] + }, + { + "name": "custom_names", + "type": [ + "array", + { + "countType": "varint", + "type": "string" + } + ] } ] ] @@ -2385,6 +2414,41 @@ } ] ], + "WindowID": [ + "mapper", + { + "type": "i8", + "mappings": { + "0": "inventory", + "1": "first", + "100": "last", + "119": "offhand", + "120": "armor", + "121": "creative", + "122": "hotbar", + "123": "fixed_inventory", + "124": "ui", + "-100": "drop_contents", + "-24": "beacon", + "-23": "trading_output", + "-22": "trading_use_inputs", + "-21": "trading_input_2", + "-20": "trading_input_1", + "-17": "enchant_output", + "-16": "enchant_material", + "-15": "enchant_input", + "-13": "anvil_output", + "-12": "anvil_result", + "-11": "anvil_material", + "-10": "container_input", + "-5": "crafting_use_ingredient", + "-4": "crafting_result", + "-3": "crafting_remove_ingredient", + "-2": "crafting_add_ingredient", + "-1": "none" + } + } + ], "WindowType": [ "mapper", { @@ -3063,6 +3127,10 @@ { "name": "source_name", "type": "string" + }, + { + "name": "message", + "type": "string" } ] ], @@ -3072,6 +3140,10 @@ { "name": "source_name", "type": "string" + }, + { + "name": "message", + "type": "string" } ] ], @@ -3081,6 +3153,10 @@ { "name": "source_name", "type": "string" + }, + { + "name": "message", + "type": "string" } ] ], @@ -4166,7 +4242,7 @@ }, { "name": "windows_id", - "type": "u8" + "type": "WindowID" } ] ], @@ -4457,8 +4533,8 @@ "type": "u8" }, { - "name": "type", - "type": "u8" + "name": "window_type", + "type": "WindowType" }, { "name": "coordinates", @@ -4492,7 +4568,7 @@ }, { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "select_slot", @@ -4517,7 +4593,7 @@ "container", [ { - "name": "inventory_id", + "name": "window_id", "type": "varint" }, { @@ -4539,7 +4615,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "property", @@ -4577,11 +4653,21 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "recipe_type", - "type": "zigzag32" + "type": [ + "mapper", + { + "type": "zigzag32", + "mappings": { + "0": "inventory", + "1": "crafting", + "2": "workbench" + } + } + ] }, { "name": "recipe_id", @@ -4589,11 +4675,23 @@ }, { "name": "input", - "type": "ItemStacks" + "type": [ + "array", + { + "countType": "varint", + "type": "Item" + } + ] }, { "name": "result", - "type": "ItemStacks" + "type": [ + "array", + { + "countType": "varint", + "type": "Item" + } + ] } ] ], @@ -4813,7 +4911,7 @@ "type": [ "mapper", { - "type": "varint", + "type": "zigzag32", "mappings": { "0": "achievement_awarded", "1": "entity_interact", @@ -4840,6 +4938,10 @@ { "name": "use_player_id", "type": "u8" + }, + { + "name": "event_data", + "type": "restBuffer" } ] ], @@ -6220,11 +6322,37 @@ ], "packet_level_event_generic": [ "container", - [] + [ + { + "name": "event_id", + "type": "varint" + }, + { + "name": "nbt", + "type": "nbtLoop" + } + ] ], "packet_lectern_update": [ "container", - [] + [ + { + "name": "page", + "type": "u8" + }, + { + "name": "page_count", + "type": "u8" + }, + { + "name": "position", + "type": "vec3i" + }, + { + "name": "drop_book", + "type": "bool" + } + ] ], "packet_video_stream_connect": [ "container", @@ -6306,12 +6434,20 @@ "packet_client_cache_blob_status": [ "container", [ + { + "name": "misses", + "type": "varint" + }, + { + "name": "haves", + "type": "varint" + }, { "name": "missing", "type": [ "array", { - "countType": "varint", + "count": "misses", "type": "lu64" } ] @@ -6321,7 +6457,7 @@ "type": [ "array", { - "countType": "varint", + "count": "haves", "type": "lu64" } ] @@ -6892,6 +7028,12 @@ "countType": "varint" } ], + "SignedByteArray": [ + "buffer", + { + "countType": "zigzag32" + } + ], "LittleString": [ "pstring", { @@ -6969,7 +7111,7 @@ "InputFlag": [ "bitflags", { - "type": "varint64", + "type": "varint", "flags": { "ascend": 1, "descend": 2, diff --git a/package.json b/package.json index b5647f9..043dc14 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "jsonwebtoken": "^8.5.1", "jsp-raknet": "github:extremeheat/raknet#client", "minecraft-folder-path": "^1.1.0", - "prismarine-nbt": "github:extremeheat/prismarine-nbt#le", + "prismarine-nbt": "^1.5.0", "protodef": "github:extremeheat/node-protodef#compiler", - "raknet-native": "^0.0.4", + "raknet-native": "^0.1.0", "uuid-1345": "^0.99.7" }, "devDependencies": { diff --git a/src/connection.js b/src/connection.js index e621446..367896c 100644 --- a/src/connection.js +++ b/src/connection.js @@ -35,9 +35,9 @@ class Connection extends EventEmitter { queue(name, params) { this.outLog('Q <- ', name, params) const packet = this.serializer.createPacketBuffer({ name, params }) - if (name == 'level_chunk') { - // Skip queue - this.sendMCPE(packet) + if (name == 'level_chunk' || name=='client_cache_blob_status' || name == 'client_cache_miss_response') { + // Skip queue, send ASAP + this.sendBuffer(packet) return } this.q.push(packet) @@ -54,12 +54,12 @@ class Connection extends EventEmitter { // For now, we're over conservative so send max 3 packets // per batch and hold the rest for the next tick const sending = [] - for (let i = 0; i < 3 && i < this.q.length; i++) { + for (let i = 0; i < this.q.length; i++) { const packet = this.q.shift() sending.push(this.q2.shift()) batch.addEncodedPacket(packet) } - // console.warn('~~ Sending', sending) + // this.outLog('~~ Sending', sending) if (this.encryptionEnabled) { this.sendEncryptedBatch(batch) } else { @@ -67,7 +67,7 @@ class Connection extends EventEmitter { } // this.q2 = [] } - }, 100) + }, 20) } writeRaw(name, buffer) { // skip protodef serializaion @@ -102,6 +102,7 @@ class Connection extends EventEmitter { } } else { this.q.push(buffer) + this.q2.push('rawBuffer') } } diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 8d1e10f..a4ab558 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -1,9 +1,6 @@ const UUID = require('uuid-1345') const minecraft = require('./minecraft') - -const Read = {} -const Write = {} -const SizeOf = {} +const { Read, Write, SizeOf } = require('./varlong') /** * UUIDs @@ -38,6 +35,39 @@ SizeOf.restBuffer = ['native', (value) => { return value.length }] +/** + * Read NBT until end of buffer or \0 + */ +Read.nbtLoop = ['context', (buffer, offset) => { + const values = [] + while (buffer[offset] != 0) { + // console.log('offs',offset, buffer.length,buffer.slice(offset)) + const n = ctx.nbt(buffer, offset) + // console.log('read',n) + values.push(n.value) + offset += n.size + } + // console.log('Ext',offset, buffer.length,buffer.slice(offset)) + return { value: values, size: buffer.length - offset } +}] +Write.nbtLoop = ['context', (value, buffer, offset) => { + for (const val of value) { + // console.log('val',val,offset) + offset = ctx.nbt(val, buffer, offset) + } + // offset += 1 + // console.log('writing 0', offset) + buffer.writeUint8(0, offset) + return offset + 1 +}] +SizeOf.nbtLoop = ['context', (value, buffer, offset) => { + let size = 1 + for (const val of value) { + size += ctx.nbt(val, buffer, offset) + } + return size +}] + /** * NBT */ @@ -50,19 +80,19 @@ SizeOf.nbt = ['native', minecraft.nbt[2]] */ // nvm, // Read.bitflags = ['parametrizable', (compiler, { type, flags }) => { - // return compiler.wrapCode(` - // const { value, size } = ${compiler.callType('buffer, offset', type)} - // const val = {} - // for (let i = 0; i < size; i++) { - // const hi = (value >> i) & 1 - // if () - // const v = value & - // if (flags[i]) - // } - // ` +// return compiler.wrapCode(` +// const { value, size } = ${compiler.callType('buffer, offset', type)} +// const val = {} +// for (let i = 0; i < size; i++) { +// const hi = (value >> i) & 1 +// if () +// const v = value & +// if (flags[i]) +// } +// ` // }] -Read.bitflags = ['parametrizable', (compiler, { type, flags }) => { +Read.bitflags = ['parametrizable', (compiler, { type, flags }) => { return compiler.wrapCode(` const { value: _value, size } = ${compiler.callType(type, 'offset')} const value = { _value } @@ -75,7 +105,7 @@ Read.bitflags = ['parametrizable', (compiler, { type, flags }) => { }] -Write.bitflags = ['parametrizable', (compiler, { type, flags }) => { +Write.bitflags = ['parametrizable', (compiler, { type, flags }) => { return compiler.wrapCode(` const flags = ${JSON.stringify(flags)} let val = value._value @@ -86,7 +116,7 @@ Write.bitflags = ['parametrizable', (compiler, { type, flags }) => { `.trim()) }] -SizeOf.bitflags = ['parametrizable', (compiler, { type, flags }) => { +SizeOf.bitflags = ['parametrizable', (compiler, { type, flags }) => { return compiler.wrapCode(` const flags = ${JSON.stringify(flags)} let val = value._value @@ -117,7 +147,7 @@ Write.enum_size_based_on_values_len = ['parametrizable', (compiler) => { }) }] SizeOf.enum_size_based_on_values_len = ['parametrizable', (compiler) => { - return str(() => { + return str(() => { if (value.values_len <= 0xff) _enum_type = 'byte' else if (value.values_len <= 0xffff) _enum_type = 'short' else if (value.values_len <= 0xffffff) _enum_type = 'int' diff --git a/src/datatypes/varlong.js b/src/datatypes/varlong.js new file mode 100644 index 0000000..fa1059f --- /dev/null +++ b/src/datatypes/varlong.js @@ -0,0 +1,63 @@ +function sizeOfVarLong(value) { + if (typeof value.valueOf() === 'object') { + value = (BigInt(value[0]) << 32n) | BigInt(value[1]) + } else if (typeof value !== 'bigint') value = BigInt(value) + + let cursor = 0 + while (value > 127n) { + value >>= 7n + cursor++ + } + return cursor + 1 +} + +/** + * Reads a 64-bit VarInt as a BigInt + */ +function readVarLong(buffer, offset) { + let result = BigInt(0) + let shift = 0n + let cursor = offset + let size = 0 + + while (true) { + if (cursor + 1 > buffer.length) { throw new Error('unexpected buffer end') } + const b = buffer.readUInt8(cursor) + result |= (BigInt(b) & 0x7fn) << shift // Add the bits to our number, except MSB + cursor++ + if (!(b & 0x80)) { // If the MSB is not set, we return the number + size = cursor - offset + break + } + shift += 7n // we only have 7 bits, MSB being the return-trigger + if (shift > 63n) throw new Error(`varint is too big: ${shift}`) + } + + return { value: result, size } +} + +/** + * Writes a zigzag encoded 64-bit VarInt as a BigInt + */ +function writeVarLong(value, buffer, offset) { + // if an array, turn it into a BigInt + if (typeof value.valueOf() === 'object') { + value = BigInt.asIntN(64, (BigInt(value[0]) << 32n)) | BigInt(value[1]) + } else if (typeof value !== 'bigint') value = BigInt(value) + + let cursor = 0 + while (value > 127n) { // keep writing in 7 bit slices + const num = Number(value & 0xFFn) + buffer.writeUInt8(num | 0x80, offset + cursor) + cursor++ + value >>= 7n + } + buffer.writeUInt8(Number(value), offset + cursor) + return offset + cursor + 1 +} + +module.exports = { + Read: { varint64: ['native', readVarLong] }, + Write: { varint64: ['native', writeVarLong] }, + SizeOf: { varint64: ['native', sizeOfVarLong] } +} \ No newline at end of file diff --git a/src/rak.js b/src/rak.js index 5a16182..ad216a0 100644 --- a/src/rak.js +++ b/src/rak.js @@ -18,7 +18,7 @@ class RakNativeClient extends EventEmitter { this.raknet = new Client(options.hostname, options.port, 'minecraft') this.raknet.on('encapsulated', thingy => { - console.log('Encap',thingy) + // console.log('Encap',thingy) const { buffer, address, guid }=thingy this.onEncapsulated(buffer, address) }) @@ -76,7 +76,7 @@ class RakNativeServer extends EventEmitter { this.raknet.on('encapsulated', (thingy) => { const { buffer, address, guid }=thingy - console.log('ENCAP',thingy) + // console.log('ENCAP',thingy) this.onEncapsulated(buffer, address) }) } diff --git a/src/relay.js b/src/relay.js index 6fdc890..0fee6af 100644 --- a/src/relay.js +++ b/src/relay.js @@ -1,4 +1,5 @@ -process.env.DEBUG = 'minecraft-protocol raknet' +// process.env.DEBUG = 'minecraft-protocol raknet' +const fs = require('fs') const { Client } = require("./client") const { Server } = require("./server") const { Player } = require("./serverPlayer") @@ -6,6 +7,8 @@ const debug = require('debug')('minecraft-protocol relay') /** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */ +const debugging = true // Do re-encoding tests + class RelayPlayer extends Player { constructor(server, conn) { super(server, conn) @@ -13,10 +16,8 @@ class RelayPlayer extends Player { this.conn = conn this.startRelaying = false - this.once('join', () => { - this.write('client_cache_status', {enabled:false}) // disable this asap on join - - this.flushDownQueue() + this.once('join', () => { // The client has joined our proxy + this.flushDownQueue() // Send queued packets from the upstream backend this.startRelaying = true }) this.downQ = [] @@ -26,6 +27,20 @@ class RelayPlayer extends Player { this.downInLog = (...msg) => console.info('** Client -> Proxy', ...msg) this.downOutLog = (...msg) => console.info('** Proxy -> Client', ...msg) + if (!server.options.logging) { + this.upInLog = () => { } + this.upOutLog = () => { } + this.downInLog = () => { } + this.downOutLog = () => { } + } + + // this.upOutLog = (...msg) => { + // if (msg.includes('player_auth_input')) { + // // stream.write(msg) + // console.info('INPUT', msg) + // } + // } + this.outLog = this.downOutLog this.inLog = this.downInLog } @@ -41,22 +56,26 @@ class RelayPlayer extends Player { const des = this.server.deserializer.parsePacketBuffer(packet) const name = des.data.name const params = des.data.params - this.upInLog('~~ Bounce B->C', name, serialize(params).slice(0, 100)) - this.upInLog('~~ ', des.buffer) - if (name == 'play_status' && params.status == 'login_success') return + this.upInLog('~ Bounce B->C', name, serialize(params).slice(0, 100)) + // this.upInLog('~ ', des.buffer) + if (name == 'play_status' && params.status == 'login_success') return // We already sent this, this needs to be sent ASAP or client will disconnect - if (name == 'level_chunk') { //send chunk directly - this.upInLog('Would send chunk', params) - this.sendBuffer(packet) - return - } else this.upInLog('?',name) + if (debugging) { // some packet encode/decode testing stuff + const rpacket = this.server.serializer.createPacketBuffer({ name, params }) + if (rpacket.toString('hex') !== packet.toString('hex')) { + console.warn('New', rpacket.toString('hex')) + console.warn('Old', packet.toString('hex')) + console.log('Failed to re-encode', name, params) + process.exit(1) + throw Error('re-encoding fail for' + name + ' - ' + JSON.stringify(params)) + } + } - // if (name == 'network_chunk_publisher_update') return - // if (name == 'crafting_data' || name == 'level_chunk') return // Alex breaks this.queue(name, params) // this.sendBuffer(packet) } + // Send queued packets to the connected client flushDownQueue() { for (const packet of this.downQ) { const des = this.server.deserializer.parsePacketBuffer(packet) @@ -65,11 +84,12 @@ class RelayPlayer extends Player { this.downQ = [] } + // Send queued packets to the backend upstream server from the client flushUpQueue() { for (var e of this.upQ) { // Send the queue const des = this.server.deserializer.parsePacketBuffer(e) - if (des.data.name == 'client_cache_status') { // already disabled on join - // this.upstream.write('client_cache_status', {enabled:false}) + if (des.data.name == 'client_cache_status') { // Currently broken, force off the chunk cache + this.upstream.write('client_cache_status', { enabled: false }) } else { this.upstream.write(des.data.name, des.data.params) } @@ -79,25 +99,35 @@ class RelayPlayer extends Player { // Called when the server gets a packet from the downstream player (Client -> PROXY -> Backend) readPacket(packet) { - if (this.startRelaying) { // The DS client conn is established & we got a packet to send to US server + if (this.startRelaying) { // The downstream client conn is established & we got a packet to send to upstream server if (!this.upstream) { // Upstream is still connecting/handshaking - debug('Got downstream connected packet but upstream is not connected yet, added to q', this.queue.length) + this.downInLog('Got downstream connected packet but upstream is not connected yet, added to q', this.upQ.length) this.upQ.push(packet) // Put into a queue return } this.flushUpQueue() // Send queued packets this.downInLog('Recv packet', packet) const des = this.server.deserializer.parsePacketBuffer(packet) + + if (debugging) { // some packet encode/decode testing stuff + const rpacket = this.server.serializer.createPacketBuffer(des.data) + if (rpacket.toString('hex') !== packet.toString('hex')) { + console.warn('New', rpacket.toString('hex')) + console.warn('Old', packet.toString('hex')) + console.log('Failed to re-encode', des.data) + process.exit(1) + } + } + switch (des.data.name) { case 'client_cache_status': - this.upstream.queue('client_cache_status', {enabled:false}) + this.upstream.queue('client_cache_status', { enabled: false }) break - // case 'request_chunk_radius': - // this.upstream.queue('request_chunk_radius', {chunk_radius: 1}) - // break default: // Emit the packet as-is back to the upstream server - this.upstream.queue(des.data.name, des.data.params) + // this.upstream.queue(des.data.name, des.data.params) + this.downInLog('Relaying', des.data) + this.upstream.sendBuffer(packet) } } else { super.readPacket(packet) @@ -150,15 +180,13 @@ class Relay extends Server { conn.close() } else { const player = new this.RelayPlayer(this, conn) - console.log('NEW CONNECTION', conn.address) + console.debug('New connection from', conn.address) this.clients[conn.address] = player this.emit('connect', { client: player }) this.openUpstreamConnection(player, conn.address) } } } -console.log = () => {} - function serialize(obj = {}, fmt) { return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt) @@ -192,7 +220,7 @@ function createRelay() { destination: { hostname: '127.0.0.1', port: 19132, - encryption: true + // encryption: true } }) diff --git a/test/serialization.js b/test/serialization.js index 6c1cbe5..bde8b37 100644 --- a/test/serialization.js +++ b/test/serialization.js @@ -241,13 +241,50 @@ function test() { } + function testLevelEventGeneric() { + const s = '7cce1f0305436f756e74800205084469725363616c65cdcc4c3e0504456e647800a01c440504456e647900001a420504456e647a00008942050653746172747899ef1944050653746172747900002042050653746172747a4583a042050a566172696174696f6e789a99193f050a566172696174696f6e799a99394000' + const buf = Buffer.from(s, 'hex') + const des = read(buf) + console.log(JSON.stringify(des)) + + console.log(des.data.name) + const newBuf = write(des.data.name, des.data.params) + console.log(newBuf.toString('hex'), s) + console.assert(newBuf.toString('hex')==s) + } + + function testEvent() { + const s = '41fdffffff5f2801001203e1417678933fdf294642a034a03e57b65b40' + const buf = Buffer.from(s, 'hex') + const des = read(buf) + console.log(serialize(des)) + + console.log(des.data.name) + const newBuf = write(des.data.name, des.data.params) + console.log(newBuf.toString('hex'), s) + console.assert(newBuf.toString('hex')==s) + } + + function testItemStackReq() { + const s = '93010105030b9d050e01920502000000000100013b32053a000000' + const buf = Buffer.from(s, 'hex') + const des = read(buf) + console.log(serialize(des)) + + console.log(des.data.name) + const newBuf = write(des.data.name, des.data.params) + console.log(newBuf.toString('hex'), 'OLD:', s) + console.assert(newBuf.toString('hex')==s) + } + // creativeTst() // availableCommands() // avaliableCmd() // creativeTestNew() // biomeDefinitions() // startGame() - testInventory() + // testLevelEventGeneric() + testItemStackReq() } if (!module.parent) {