Merge pull request #60 from extremeheat/examples
Add viewer, protocol and crypto updates
This commit is contained in:
commit
0bf6b8bded
33 changed files with 1171 additions and 192 deletions
|
|
@ -0,0 +1 @@
|
|||
examples/viewer
|
||||
|
|
@ -840,11 +840,11 @@
|
|||
}
|
||||
]
|
||||
],
|
||||
"creative": [
|
||||
"craft": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "inventory_id",
|
||||
"name": "action",
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
|
|
@ -858,15 +858,6 @@
|
|||
}
|
||||
]
|
||||
],
|
||||
"craft": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "action",
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
],
|
||||
"craft_slot": [
|
||||
"container",
|
||||
[
|
||||
|
|
@ -1083,8 +1074,8 @@
|
|||
"container",
|
||||
[
|
||||
{
|
||||
"name": "runtime_id",
|
||||
"type": "zigzag32"
|
||||
"name": "stack_id",
|
||||
"type": "varint"
|
||||
},
|
||||
{
|
||||
"name": "item",
|
||||
|
|
@ -1265,7 +1256,7 @@
|
|||
},
|
||||
{
|
||||
"name": "network_id",
|
||||
"type": "zigzag32"
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
@ -1310,7 +1301,7 @@
|
|||
},
|
||||
{
|
||||
"name": "network_id",
|
||||
"type": "zigzag32"
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
@ -1355,7 +1346,7 @@
|
|||
},
|
||||
{
|
||||
"name": "network_id",
|
||||
"type": "zigzag32"
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
@ -1414,7 +1405,7 @@
|
|||
},
|
||||
{
|
||||
"name": "network_id",
|
||||
"type": "zigzag32"
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
@ -1473,7 +1464,7 @@
|
|||
},
|
||||
{
|
||||
"name": "network_id",
|
||||
"type": "zigzag32"
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
@ -1524,7 +1515,7 @@
|
|||
},
|
||||
{
|
||||
"name": "network_id",
|
||||
"type": "zigzag32"
|
||||
"type": "varint"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
@ -2058,7 +2049,7 @@
|
|||
"22": "stop_swimming",
|
||||
"23": "start_spin_attack",
|
||||
"24": "stop_spin_attack",
|
||||
"25": "ineract_block",
|
||||
"25": "interact_block",
|
||||
"26": "predict_break",
|
||||
"27": "continue_break"
|
||||
}
|
||||
|
|
@ -2068,11 +2059,11 @@
|
|||
"container",
|
||||
[
|
||||
{
|
||||
"name": "container_id",
|
||||
"type": "u8"
|
||||
"name": "slot_type",
|
||||
"type": "ContainerSlotType"
|
||||
},
|
||||
{
|
||||
"name": "slot_id",
|
||||
"name": "slot",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
|
|
@ -2086,7 +2077,7 @@
|
|||
[
|
||||
{
|
||||
"name": "request_id",
|
||||
"type": "zigzag32"
|
||||
"type": "varint"
|
||||
},
|
||||
{
|
||||
"name": "actions",
|
||||
|
|
@ -2282,7 +2273,7 @@
|
|||
"container",
|
||||
[
|
||||
{
|
||||
"name": "creative_item_network_id",
|
||||
"name": "item_id",
|
||||
"type": "varint32"
|
||||
}
|
||||
]
|
||||
|
|
@ -2367,58 +2358,75 @@
|
|||
"type": "varint32"
|
||||
},
|
||||
{
|
||||
"name": "containers",
|
||||
"anon": true,
|
||||
"type": [
|
||||
"array",
|
||||
"switch",
|
||||
{
|
||||
"countType": "varint",
|
||||
"type": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "slot_type",
|
||||
"type": "ContainerSlotType"
|
||||
},
|
||||
{
|
||||
"name": "slots",
|
||||
"type": [
|
||||
"array",
|
||||
{
|
||||
"countType": "varint",
|
||||
"type": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "slot",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "hotbar_slot",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "item_stack_id",
|
||||
"type": "varint32"
|
||||
},
|
||||
{
|
||||
"name": "custom_name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "durability_correction",
|
||||
"type": "zigzag32"
|
||||
}
|
||||
"compareTo": "status",
|
||||
"fields": {
|
||||
"ok": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "containers",
|
||||
"type": [
|
||||
"array",
|
||||
{
|
||||
"countType": "varint",
|
||||
"type": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "slot_type",
|
||||
"type": "ContainerSlotType"
|
||||
},
|
||||
{
|
||||
"name": "slots",
|
||||
"type": [
|
||||
"array",
|
||||
{
|
||||
"countType": "varint",
|
||||
"type": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "slot",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "hotbar_slot",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "item_stack_id",
|
||||
"type": "varint32"
|
||||
},
|
||||
{
|
||||
"name": "custom_name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "durability_correction",
|
||||
"type": "zigzag32"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
"default": "void"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2584,7 +2592,7 @@
|
|||
"WindowType": [
|
||||
"mapper",
|
||||
{
|
||||
"type": "u8",
|
||||
"type": "i8",
|
||||
"mappings": {
|
||||
"0": "container",
|
||||
"1": "workbench",
|
||||
|
|
@ -2619,7 +2627,9 @@
|
|||
"30": "cartography",
|
||||
"31": "hud",
|
||||
"32": "jigsaw_editor",
|
||||
"33": "smithing_table"
|
||||
"33": "smithing_table",
|
||||
"-9": "none",
|
||||
"-1": "inventory"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
@ -4451,7 +4461,7 @@
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "windows_id",
|
||||
"name": "window_id",
|
||||
"type": "WindowID"
|
||||
}
|
||||
]
|
||||
|
|
@ -4499,7 +4509,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "target_runtime_entity_id",
|
||||
"name": "target_entity_id",
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
|
|
@ -4757,8 +4767,8 @@
|
|||
"container",
|
||||
[
|
||||
{
|
||||
"name": "inventory_id",
|
||||
"type": "varint"
|
||||
"name": "window_id",
|
||||
"type": "WindowIDVarint"
|
||||
},
|
||||
{
|
||||
"name": "input",
|
||||
|
|
@ -4870,7 +4880,20 @@
|
|||
],
|
||||
"packet_gui_data_pick_item": [
|
||||
"container",
|
||||
[]
|
||||
[
|
||||
{
|
||||
"name": "item_name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "item_effects",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "hotbar_slot",
|
||||
"type": "li32"
|
||||
}
|
||||
]
|
||||
],
|
||||
"packet_adventure_settings": [
|
||||
"container",
|
||||
|
|
@ -6283,14 +6306,14 @@
|
|||
},
|
||||
{
|
||||
"name": "entity_unique_id",
|
||||
"type": "varint"
|
||||
"type": "zigzag64"
|
||||
},
|
||||
{
|
||||
"name": "transition_type",
|
||||
"type": [
|
||||
"mapper",
|
||||
{
|
||||
"type": "varint",
|
||||
"type": "varint64",
|
||||
"mappings": {
|
||||
"0": "entity",
|
||||
"1": "create",
|
||||
|
|
@ -7162,7 +7185,7 @@
|
|||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "types.feet",
|
||||
"compareTo": "type.feet",
|
||||
"fields": {
|
||||
"true": "zigzag32"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -769,7 +769,7 @@ packet_mob_equipment:
|
|||
item: Item
|
||||
slot: u8
|
||||
selected_slot: u8
|
||||
windows_id: WindowID
|
||||
window_id: WindowID
|
||||
|
||||
packet_mob_armor_equipment:
|
||||
!id: 0x20
|
||||
|
|
@ -793,7 +793,7 @@ packet_interact:
|
|||
6: open_inventory
|
||||
# TargetEntityRuntimeID is the runtime ID of the entity that the player interacted with. This is empty
|
||||
# for the InteractActionOpenInventory action type.
|
||||
target_runtime_entity_id: varint64
|
||||
target_entity_id: varint64
|
||||
# Position associated with the ActionType above. For the InteractActionMouseOverEntity, this is the
|
||||
# position relative to the entity moused over over which the player hovered with its mouse/touch. For the
|
||||
# InteractActionLeaveVehicle, this is the position that the player spawns at after leaving the vehicle.
|
||||
|
|
@ -845,10 +845,16 @@ packet_set_entity_data:
|
|||
metadata: MetadataDictionary
|
||||
tick: varint
|
||||
|
||||
# SetActorMotion is sent by the server to change the client-side velocity of an entity. It is usually used
|
||||
# in combination with server-side movement calculation.
|
||||
packet_set_entity_motion:
|
||||
!id: 0x28
|
||||
!bound: both
|
||||
# EntityRuntimeID is the runtime ID of the entity. The runtime ID is unique for each world session, and
|
||||
# entities are generally identified in packets using this runtime ID.
|
||||
runtime_entity_id: varint64
|
||||
# Velocity is the new velocity the entity gets. This velocity will initiate the client-side movement of
|
||||
# the entity.
|
||||
velocity: vec3f
|
||||
|
||||
# SetActorLink is sent by the server to initiate an entity link client-side, meaning one entity will start
|
||||
|
|
@ -941,7 +947,7 @@ packet_inventory_content:
|
|||
!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
|
||||
window_id: WindowIDVarint
|
||||
# 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
|
||||
|
|
@ -1007,10 +1013,19 @@ packet_crafting_event:
|
|||
# Output is a list of items that were obtained as a result of crafting the recipe.
|
||||
result: Item[]varint
|
||||
|
||||
|
||||
# GUIDataPickItem is sent by the server to make the client 'select' a hot bar slot. It currently appears to
|
||||
# be broken however, and does not actually set the selected slot to the hot bar slot set in the packet.
|
||||
packet_gui_data_pick_item:
|
||||
!id: 0x36
|
||||
!bound: client
|
||||
# ItemName is the name of the item that shows up in the top part of the popup that shows up when
|
||||
# selecting an item. It is shown as if an item was selected by the player itself.
|
||||
item_name: string
|
||||
# ItemEffects is the line under the ItemName, where the effects of the item are usually situated.
|
||||
item_effects: string
|
||||
# HotBarSlot is the hot bar slot to be selected/picked. This does not currently work, so it does not
|
||||
# matter what number this is.
|
||||
hotbar_slot: li32
|
||||
|
||||
# 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
|
||||
|
|
@ -1745,11 +1760,11 @@ packet_update_block_synced:
|
|||
# entity transitions from.
|
||||
# Note that for both possible values for TransitionType, the EntityUniqueID should point to the falling
|
||||
# block entity involved.
|
||||
entity_unique_id: varint
|
||||
entity_unique_id: zigzag64
|
||||
# TransitionType is the type of the transition that happened. It is either BlockToEntityTransition, when
|
||||
# a block placed becomes a falling entity, or EntityToBlockTransition, when a falling entity hits the
|
||||
# ground and becomes a solid block again.
|
||||
transition_type: varint =>
|
||||
transition_type: varint64 =>
|
||||
# For falling sand, when a sand turns to an entity
|
||||
0: entity
|
||||
# When sand turns back to a new block
|
||||
|
|
@ -2231,7 +2246,7 @@ packet_player_armor_damage:
|
|||
if true: zigzag32
|
||||
leggings_damage: type.legs ?
|
||||
if true: zigzag32
|
||||
boots_damage: types.feet ?
|
||||
boots_damage: type.feet ?
|
||||
if true: zigzag32
|
||||
|
||||
ArmorDamageType: [ "bitflags",
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ TransactionActions:
|
|||
100: craft_slot
|
||||
99999: craft
|
||||
_: source_type?
|
||||
if container or creative:
|
||||
if container or craft:
|
||||
inventory_id: varint
|
||||
if world_interaction:
|
||||
flags: varint
|
||||
|
|
@ -548,7 +548,7 @@ ItemStack:
|
|||
# StackNetworkID is the network ID of the item stack. If the stack is empty, 0 is always written for this
|
||||
# field. If not, the field should be set to 1 if the server authoritative inventories are disabled in the
|
||||
# StartGame packet, or to a unique stack ID if it is enabled.
|
||||
runtime_id: zigzag32
|
||||
stack_id: varint
|
||||
# Stack is the actual item stack of the item instance.
|
||||
item: Item
|
||||
|
||||
|
|
@ -577,16 +577,16 @@ PotionContainerChangeRecipes: []varint
|
|||
|
||||
Recipes: []varint
|
||||
type: zigzag32 =>
|
||||
'0': 'shapeless' #'ENTRY_SHAPELESS',
|
||||
'1': 'shaped' #'ENTRY_SHAPED',
|
||||
'2': 'furnace' # 'ENTRY_FURNACE',
|
||||
0: shapeless #'ENTRY_SHAPELESS',
|
||||
1: shaped #'ENTRY_SHAPED',
|
||||
2: furnace # 'ENTRY_FURNACE',
|
||||
# `furnace_with_metadata` is a recipe specifically used for furnace-type crafting stations. It is equal to
|
||||
# `furnace`, except it has an input item with a specific metadata value, instead of any metadata value.
|
||||
'3': 'furnace_with_metadata' # 'ENTRY_FURNACE_DATA', // has metadata
|
||||
'4': 'multi' #'ENTRY_MULTI', //TODO
|
||||
'5': 'shulker_box' #'ENTRY_SHULKER_BOX', //TODO
|
||||
'6': 'shapeless_chemistry' #'ENTRY_SHAPELESS_CHEMISTRY', //TODO
|
||||
'7': 'shaped_chemistry' #'ENTRY_SHAPED_CHEMISTRY', //TODO
|
||||
3: furnace_with_metadata # 'ENTRY_FURNACE_DATA', // has metadata
|
||||
4: multi #'ENTRY_MULTI', //TODO
|
||||
5: shulker_box #'ENTRY_SHULKER_BOX', //TODO
|
||||
6: shapeless_chemistry #'ENTRY_SHAPELESS_CHEMISTRY', //TODO
|
||||
7: shaped_chemistry #'ENTRY_SHAPED_CHEMISTRY', //TODO
|
||||
recipe: type?
|
||||
if shapeless or shulker_box or shapeless_chemistry:
|
||||
recipe_id: string
|
||||
|
|
@ -595,7 +595,7 @@ Recipes: []varint
|
|||
uuid: uuid
|
||||
block: string
|
||||
priority: zigzag32
|
||||
network_id: zigzag32
|
||||
network_id: varint
|
||||
if shaped or shaped_chemistry:
|
||||
recipe_id: string
|
||||
width: zigzag32
|
||||
|
|
@ -608,7 +608,7 @@ Recipes: []varint
|
|||
uuid: uuid
|
||||
block: string
|
||||
priority: zigzag32
|
||||
network_id: zigzag32
|
||||
network_id: varint
|
||||
if furnace:
|
||||
input_id: zigzag32
|
||||
output: Item
|
||||
|
|
@ -620,7 +620,7 @@ Recipes: []varint
|
|||
block: string
|
||||
if multi:
|
||||
uuid: uuid
|
||||
network_id: zigzag32
|
||||
network_id: varint
|
||||
|
||||
SkinImage:
|
||||
width: li32
|
||||
|
|
@ -748,13 +748,20 @@ Action: zigzag32 =>
|
|||
22: stop_swimming
|
||||
23: start_spin_attack
|
||||
24: stop_spin_attack
|
||||
25: ineract_block
|
||||
25: interact_block
|
||||
26: predict_break
|
||||
27: continue_break
|
||||
|
||||
# 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.
|
||||
StackRequestSlotInfo:
|
||||
container_id: u8
|
||||
slot_id: u8
|
||||
# ContainerID is the ID of the container that the slot was in.
|
||||
slot_type: ContainerSlotType
|
||||
# Slot is the index of the slot within the container with the ContainerID above.
|
||||
slot: u8
|
||||
# StackNetworkID is the unique stack ID that the client assumes to be present in this slot. The server
|
||||
# must check if these IDs match. If they do not match, servers should reject the stack request that the
|
||||
# action holding this info was in.
|
||||
stack_id: zigzag32
|
||||
|
||||
# ItemStackRequest is sent by the client to change item stacks in an inventory. It is essentially a
|
||||
|
|
@ -764,7 +771,7 @@ StackRequestSlotInfo:
|
|||
ItemStackRequest:
|
||||
# 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
|
||||
request_id: varint
|
||||
actions: []varint
|
||||
type_id: u8 =>
|
||||
# TakeStackRequestAction is sent by the client to the server to take x amount of items from one slot in a
|
||||
|
|
@ -872,9 +879,9 @@ ItemStackRequest:
|
|||
# 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
|
||||
# The stack ID of the creative item that is being created. This is one of the
|
||||
# creative item stack IDs sent in the CreativeContent packet.
|
||||
item_id: varint32
|
||||
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
|
||||
|
|
@ -901,30 +908,32 @@ ItemStackResponses: []varint
|
|||
# RequestID is the unique ID of the request that this response is in reaction to. If rejected, the client
|
||||
# will undo the actions from the request with this ID.
|
||||
request_id: varint32
|
||||
# ContainerInfo holds information on the containers that had their contents changed as a result of the
|
||||
# request.
|
||||
containers: []varint
|
||||
# ContainerID is the container ID of the container that the slots that follow are in. For the main
|
||||
# inventory, this value seems to be 0x1b. For the cursor, this value seems to be 0x3a. For the crafting
|
||||
# grid, this value seems to be 0x0d.
|
||||
# * actually, this is ContainerSlotType - used by the inventory system that specifies the type of slot
|
||||
slot_type: ContainerSlotType
|
||||
# SlotInfo holds information on what item stack should be present in specific slots in the container.
|
||||
slots: []varint
|
||||
# Slot and HotbarSlot seem to be the same value every time: The slot that was actually changed. I'm not
|
||||
# sure if these slots ever differ.
|
||||
slot: u8
|
||||
hotbar_slot: u8
|
||||
# Count is the total count of the item stack. This count will be shown client-side after the response is
|
||||
# sent to the client.
|
||||
count: u8
|
||||
# StackNetworkID is the network ID of the new stack at a specific slot.
|
||||
item_stack_id: varint32
|
||||
# CustomName is the custom name of the item stack. It is used in relation to text filtering.
|
||||
custom_name: string
|
||||
# DurabilityCorrection is the current durability of the item stack. This durability will be shown
|
||||
# client-side after the response is sent to the client.
|
||||
durability_correction: zigzag32
|
||||
_: status ?
|
||||
if ok:
|
||||
# ContainerInfo holds information on the containers that had their contents changed as a result of the
|
||||
# request.
|
||||
containers: []varint
|
||||
# ContainerID is the container ID of the container that the slots that follow are in. For the main
|
||||
# inventory, this value seems to be 0x1b. For the cursor, this value seems to be 0x3a. For the crafting
|
||||
# grid, this value seems to be 0x0d.
|
||||
# * actually, this is ContainerSlotType - used by the inventory system that specifies the type of slot
|
||||
slot_type: ContainerSlotType
|
||||
# SlotInfo holds information on what item stack should be present in specific slots in the container.
|
||||
slots: []varint
|
||||
# Slot and HotbarSlot seem to be the same value every time: The slot that was actually changed. I'm not
|
||||
# sure if these slots ever differ.
|
||||
slot: u8
|
||||
hotbar_slot: u8
|
||||
# Count is the total count of the item stack. This count will be shown client-side after the response is
|
||||
# sent to the client.
|
||||
count: u8
|
||||
# StackNetworkID is the network ID of the new stack at a specific slot.
|
||||
item_stack_id: varint32
|
||||
# CustomName is the custom name of the item stack. It is used in relation to text filtering.
|
||||
custom_name: string
|
||||
# DurabilityCorrection is the current durability of the item stack. This durability will be shown
|
||||
# client-side after the response is sent to the client.
|
||||
durability_correction: zigzag32
|
||||
|
||||
|
||||
ItemComponentList: []varint
|
||||
|
|
@ -1025,7 +1034,9 @@ WindowIDVarint: varint =>
|
|||
123: fixed_inventory
|
||||
124: ui
|
||||
|
||||
WindowType: u8 =>
|
||||
WindowType: i8 =>
|
||||
-9: none
|
||||
-1: inventory
|
||||
0: container
|
||||
1: workbench
|
||||
2: furnace
|
||||
|
|
@ -1061,6 +1072,7 @@ WindowType: u8 =>
|
|||
32: jigsaw_editor
|
||||
33: smithing_table
|
||||
|
||||
# Used in inventory transactions.
|
||||
ContainerSlotType: u8 =>
|
||||
- anvil_input
|
||||
- anvil_material
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function createRelay () {
|
|||
}
|
||||
})
|
||||
|
||||
relay.create()
|
||||
relay.listen()
|
||||
}
|
||||
|
||||
createRelay()
|
||||
|
|
|
|||
58
examples/viewer/client/BotProvider.js
Normal file
58
examples/viewer/client/BotProvider.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
const { Version } = require('bedrock-provider')
|
||||
const { WorldView } = require('prismarine-viewer/viewer')
|
||||
const World = require('prismarine-world')()
|
||||
const ChunkColumn = require('./Chunk')()
|
||||
const { MovementManager } = require('./movements')
|
||||
|
||||
class BotProvider extends WorldView {
|
||||
chunks = {}
|
||||
lastSentPos
|
||||
positionUpdated = true
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.connect()
|
||||
this.listenToBot()
|
||||
this.world = new World()
|
||||
this.movements = new MovementManager(this)
|
||||
|
||||
this.onKeyDown = () => {}
|
||||
this.onKeyUp = () => {}
|
||||
|
||||
this.removeAllListeners('mouseClick')
|
||||
}
|
||||
|
||||
raycast () {
|
||||
// TODO : fix
|
||||
}
|
||||
|
||||
get entity () { return this.movements.player.entity }
|
||||
|
||||
handleChunk (packet, render = true) {
|
||||
const hash = (packet.x << 4) + ',' + (packet.z << 4)
|
||||
if (this.loadChunk[hash]) return
|
||||
const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z)
|
||||
cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count).then(() => {
|
||||
this.loadedChunks[hash] = true
|
||||
this.world.setColumn(packet.x, packet.z, cc)
|
||||
const chunk = cc.serialize()
|
||||
// console.log('Chunk', chunk)
|
||||
if (render) this.emitter.emit('loadChunk', { x: packet.x << 4, z: packet.z << 4, chunk })
|
||||
})
|
||||
}
|
||||
|
||||
updatePlayerCamera (id, position, yaw, pitch, updateState) {
|
||||
this.emit('playerMove', id, { position, yaw, pitch })
|
||||
|
||||
if (updateState) {
|
||||
this.movements.updatePosition(position, yaw, pitch)
|
||||
}
|
||||
}
|
||||
|
||||
stopBot () {
|
||||
clearInterval(this.tickLoop)
|
||||
this.movements.stopPhys()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { BotProvider }
|
||||
150
examples/viewer/client/BotViewer.js
Normal file
150
examples/viewer/client/BotViewer.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/* global THREE */
|
||||
const { Viewer, MapControls } = require('prismarine-viewer/viewer')
|
||||
// const { Vec3 } = require('vec3')
|
||||
const { ClientProvider } = require('./ClientProvider')
|
||||
// const { ProxyProvider } = require('./ProxyProvider')
|
||||
global.THREE = require('three')
|
||||
|
||||
const MCVER = '1.16.1'
|
||||
|
||||
class BotViewer {
|
||||
start () {
|
||||
this.bot = new ClientProvider()
|
||||
// this.bot = new ProxyProvider()
|
||||
// Create three.js context, add to page
|
||||
this.renderer = new THREE.WebGLRenderer()
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio || 1)
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(this.renderer.domElement)
|
||||
|
||||
// Create viewer
|
||||
this.viewer = new Viewer(this.renderer)
|
||||
this.viewer.setVersion(MCVER)
|
||||
// Attach controls to viewer
|
||||
this.controls = new MapControls(this.viewer.camera, this.renderer.domElement)
|
||||
// Enable damping (inertia) on movement
|
||||
this.controls.enableDamping = true
|
||||
this.controls.dampingFactor = 0.09
|
||||
console.info('Registered handlers')
|
||||
// Link WorldView and Viewer
|
||||
this.viewer.listen(this.bot)
|
||||
|
||||
this.bot.on('spawn', ({ position, firstPerson }) => {
|
||||
// Initialize viewer, load chunks
|
||||
this.bot.init(position)
|
||||
// Start listening for keys
|
||||
this.registerBrowserEvents()
|
||||
|
||||
if (firstPerson && this.bot.movements) {
|
||||
this.viewer.camera.position.set(position.x, position.y, position.z)
|
||||
this.firstPerson = true
|
||||
this.controls.enabled = false
|
||||
} else {
|
||||
this.viewer.camera.position.set(position.x, position.y, position.z)
|
||||
}
|
||||
})
|
||||
|
||||
this.bot.on('playerMove', (id, pos) => {
|
||||
if (this.firstPerson && id < 10) {
|
||||
this.setFirstPersonCamera(pos)
|
||||
return
|
||||
}
|
||||
|
||||
window.viewer.viewer.entities.update({
|
||||
name: 'player',
|
||||
id,
|
||||
pos: pos.position,
|
||||
width: 0.6,
|
||||
height: 1.8,
|
||||
yaw: pos.yaw,
|
||||
pitch: pos.pitch
|
||||
})
|
||||
})
|
||||
|
||||
const oldFov = this.viewer.camera.fov
|
||||
const sprintFov = this.viewer.camera.fov + 20
|
||||
const sneakFov = this.viewer.camera.fov - 10
|
||||
|
||||
const onSprint = () => {
|
||||
this.viewer.camera.fov = sprintFov
|
||||
this.viewer.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
const onSneak = () => {
|
||||
this.viewer.camera.fov = sneakFov
|
||||
this.viewer.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
const onRelease = () => {
|
||||
this.viewer.camera.fov = oldFov
|
||||
this.viewer.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.bot.on('startSprint', onSprint)
|
||||
this.bot.on('startSneak', onSneak)
|
||||
this.bot.on('stopSprint', onRelease)
|
||||
this.bot.on('stopSneak', onRelease)
|
||||
|
||||
this.controls.update()
|
||||
|
||||
// Browser animation loop
|
||||
const animate = () => {
|
||||
window.requestAnimationFrame(animate)
|
||||
if (this.controls && !this.firstPerson) this.controls.update()
|
||||
this.viewer.update()
|
||||
this.renderer.render(this.viewer.scene, this.viewer.camera)
|
||||
}
|
||||
animate()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.viewer.camera.aspect = window.innerWidth / window.innerHeight
|
||||
this.viewer.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
}
|
||||
|
||||
onMouseMove = (e) => {
|
||||
if (this.firstPerson) {
|
||||
this.bot.entity.pitch -= e.movementY * 0.005
|
||||
this.bot.entity.yaw -= e.movementX * 0.004
|
||||
}
|
||||
}
|
||||
|
||||
onPointerLockChange = () => {
|
||||
const e = this.renderer.domElement
|
||||
if (document.pointerLockElement === e) {
|
||||
e.parentElement.addEventListener('mousemove', this.onMouseMove, { passive: true })
|
||||
} else {
|
||||
e.parentElement.removeEventListener('mousemove', this.onMouseMove, false)
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown = () => {
|
||||
if (this.firstPerson && !document.pointerLockElement) {
|
||||
this.renderer.domElement.requestPointerLock()
|
||||
}
|
||||
}
|
||||
|
||||
registerBrowserEvents () {
|
||||
const e = this.renderer.domElement
|
||||
e.parentElement.addEventListener('keydown', this.bot.onKeyDown)
|
||||
e.parentElement.addEventListener('keyup', this.bot.onKeyUp)
|
||||
e.parentElement.addEventListener('mousedown', this.onMouseDown)
|
||||
document.addEventListener('pointerlockchange', this.onPointerLockChange, false)
|
||||
}
|
||||
|
||||
unregisterBrowserEvents () {
|
||||
const e = this.renderer.domElement
|
||||
e.parentElement.removeEventListener('keydown', this.bot.onKeyDown)
|
||||
e.parentElement.removeEventListener('keyup', this.bot.onKeyUp)
|
||||
e.parentElement.removeEventListener('mousemove', this.onMouseMove)
|
||||
e.parentElement.removeEventListener('mousedown', this.onMouseDown)
|
||||
document.removeEventListener('pointerlockchange', this.onPointerLockChange, false)
|
||||
}
|
||||
|
||||
setFirstPersonCamera (entity) {
|
||||
this.viewer.setFirstPersonCamera(entity.position, entity.yaw, entity.pitch * 2)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { BotViewer }
|
||||
18
examples/viewer/client/Chunk.js
Normal file
18
examples/viewer/client/Chunk.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const { ChunkColumn } = require('bedrock-provider')
|
||||
|
||||
const Block = require('prismarine-block')('1.16.1')
|
||||
|
||||
class ChunkColumnWrapped extends ChunkColumn { // pchunk compatiblity wrapper
|
||||
// Block access
|
||||
setBlockStateId (pos, stateId) {
|
||||
super.setBlock(pos.x, pos.y, pos.z, Block.fromStateId(stateId))
|
||||
}
|
||||
|
||||
getBlockStateId (pos) {
|
||||
return super.getBlock(pos.x, pos.y, pos.z)?.stateId
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (version) => {
|
||||
return ChunkColumnWrapped
|
||||
}
|
||||
114
examples/viewer/client/ClientProvider.js
Normal file
114
examples/viewer/client/ClientProvider.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
const { Client } = require('bedrock-protocol')
|
||||
const { BotProvider } = require('./BotProvider')
|
||||
|
||||
const controlMap = {
|
||||
forward: ['KeyW', 'KeyZ'],
|
||||
back: 'KeyS',
|
||||
left: ['KeyA', 'KeyQ'],
|
||||
right: 'KeyD',
|
||||
sneak: 'ShiftLeft',
|
||||
jump: 'Space'
|
||||
}
|
||||
|
||||
class ClientProvider extends BotProvider {
|
||||
downKeys = new Set()
|
||||
|
||||
connect () {
|
||||
const client = new Client({ hostname: '127.0.0.1', version: '1.16.210', username: 'notch', offline: true, port: 19132, connectTimeout: 100000 })
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
|
||||
this.heartbeat = setInterval(() => {
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
})
|
||||
|
||||
this.client = client
|
||||
}
|
||||
|
||||
close () {
|
||||
this.client?.close()
|
||||
}
|
||||
|
||||
listenToBot () {
|
||||
this.client.on('connect', () => {
|
||||
console.log('Bot has connected!')
|
||||
})
|
||||
this.client.on('start_game', packet => {
|
||||
this.updatePosition(packet.player_position)
|
||||
this.movements.init('server', packet.player_position, /* vel */ null, packet.rotation.z || 0, packet.rotation.x || 0, 0)
|
||||
})
|
||||
|
||||
this.client.on('spawn', () => {
|
||||
this.movements.startPhys()
|
||||
// server allows client to render chunks & spawn in world
|
||||
this.emit('spawn', { position: this.lastPos, firstPerson: true })
|
||||
|
||||
this.tickLoop = setInterval(() => {
|
||||
this.client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
})
|
||||
|
||||
this.client.on('level_chunk', packet => {
|
||||
this.handleChunk(packet)
|
||||
})
|
||||
|
||||
this.client.on('move_player', packet => {
|
||||
if (packet.runtime_id === this.client.entityId) {
|
||||
this.movements.updatePosition(packet.position, packet.yaw, packet.pitch, packet.head_yaw, packet.tick)
|
||||
}
|
||||
})
|
||||
|
||||
this.client.on('set_entity_motion', packet => {
|
||||
// if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position)
|
||||
})
|
||||
|
||||
this.client.on('tick_sync', (packet) => {
|
||||
this.lastTick = packet.response_time
|
||||
})
|
||||
}
|
||||
|
||||
onKeyDown = (evt) => {
|
||||
const code = evt.code
|
||||
for (const control in controlMap) {
|
||||
if (controlMap[control].includes(code)) {
|
||||
this.movements.setControlState(control, true)
|
||||
break
|
||||
}
|
||||
if (evt.ctrlKey) {
|
||||
this.movements.setControlState('sprint', true)
|
||||
}
|
||||
}
|
||||
this.downKeys.add(code)
|
||||
}
|
||||
|
||||
onKeyUp = (evt) => {
|
||||
const code = evt.code
|
||||
if (code === 'ControlLeft' && this.downKeys.has('ControlLeft')) {
|
||||
this.movements.setControlState('sprint', false)
|
||||
}
|
||||
for (const control in controlMap) {
|
||||
if (controlMap[control].includes(code)) {
|
||||
this.movements.setControlState(control, false)
|
||||
break
|
||||
}
|
||||
}
|
||||
this.downKeys.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ClientProvider }
|
||||
89
examples/viewer/client/ProxyProvider.js
Normal file
89
examples/viewer/client/ProxyProvider.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
const { Relay } = require('bedrock-protocol')
|
||||
const { BotProvider } = require('./BotProvider')
|
||||
const { diff } = require('./util')
|
||||
|
||||
class ProxyProvider extends BotProvider {
|
||||
lastPlayerMovePacket
|
||||
|
||||
connect () {
|
||||
const proxy = new Relay({
|
||||
hostname: '0.0.0.0',
|
||||
port: 19130,
|
||||
// logging: true,
|
||||
destination: {
|
||||
hostname: '127.0.0.1',
|
||||
port: 19132
|
||||
}
|
||||
})
|
||||
proxy.listen()
|
||||
console.info('Waiting for connect')
|
||||
|
||||
proxy.on('join', (client, server) => {
|
||||
client.on('clientbound', ({ name, params }) => {
|
||||
if (name === 'level_chunk') {
|
||||
this.handleChunk(params, true)
|
||||
} else if (name === 'start_game') {
|
||||
this.movements.init('', params.player_position, null, params.rotation.z, params.rotation.x, 0)
|
||||
} else if (name === 'play_status') {
|
||||
this.movements.startPhys()
|
||||
this.emit('spawn', { position: this.movements.lastPos, firstPerson: true })
|
||||
console.info('Started physics!')
|
||||
} else if (name === 'move_player') {
|
||||
console.log('move_player', params)
|
||||
this.movements.updatePosition(params.position, params.yaw, params.pitch, params.head_yaw, params.tick)
|
||||
}
|
||||
|
||||
if (name.includes('entity') || name.includes('network_chunk_publisher_update') || name.includes('tick') || name.includes('level')) return
|
||||
console.log('CB', name)
|
||||
})
|
||||
|
||||
client.on('serverbound', ({ name, params }) => {
|
||||
// { name, params }
|
||||
if (name === 'player_auth_input') {
|
||||
this.movements.pushInputState(params.input_data, params.yaw, params.pitch)
|
||||
this.movements.pushCameraControl(params, 1)
|
||||
|
||||
// Log Movement deltas
|
||||
{
|
||||
this.lastMovePacket = params
|
||||
if (this.firstPlayerMovePacket) {
|
||||
const id = diff(this.firstPlayerMovePacket.input_data, params.input_data)
|
||||
const md = diff(this.firstPlayerMovePacket.move_vector, params.move_vector)
|
||||
const dd = diff(this.firstPlayerMovePacket.delta, params.delta)
|
||||
if (id || md) {
|
||||
if (globalThis.logging) console.log('Move', params.position, id, md, dd)
|
||||
globalThis.movements ??= []
|
||||
globalThis.movements.push(params)
|
||||
}
|
||||
}
|
||||
if (!this.firstPlayerMovePacket) {
|
||||
this.firstPlayerMovePacket = params
|
||||
for (const key in params.input_data) {
|
||||
params.input_data[key] = false
|
||||
}
|
||||
params.input_data._value = 0n
|
||||
params.move_vector = { x: 0, z: 0 }
|
||||
params.delta = { x: 0, y: 0, z: 0 }
|
||||
}
|
||||
}
|
||||
} else if (!name.includes('tick') && !name.includes('level')) {
|
||||
console.log('Sending', name)
|
||||
}
|
||||
})
|
||||
console.info('Client and Server Connected!')
|
||||
})
|
||||
|
||||
this.proxy = proxy
|
||||
}
|
||||
|
||||
listenToBot () {
|
||||
|
||||
}
|
||||
|
||||
close () {
|
||||
this.proxy?.close()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ProxyProvider }
|
||||
globalThis.logging = true
|
||||
22
examples/viewer/client/app.css
Normal file
22
examples/viewer/client/app.css
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
canvas {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
22
examples/viewer/client/index.html
Normal file
22
examples/viewer/client/index.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Prismarine Viewer</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id='splash'>
|
||||
<div class='header'>Prismarine Viewer</div>
|
||||
|
||||
<div>
|
||||
<div>Connecting to 127.0.0.1, port 19132...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4
examples/viewer/client/index.js
Normal file
4
examples/viewer/client/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const { BotViewer } = require('./BotViewer')
|
||||
|
||||
global.viewer = new BotViewer()
|
||||
global.viewer.start()
|
||||
305
examples/viewer/client/movements.js
Normal file
305
examples/viewer/client/movements.js
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
const { Physics, PlayerState } = require('prismarine-physics')
|
||||
const { performance } = require('perf_hooks')
|
||||
const { d2r, r2d } = require('./util')
|
||||
const vec3 = require('vec3')
|
||||
|
||||
const PHYSICS_INTERVAL_MS = 50
|
||||
const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000
|
||||
const AXES = ['forward', 'back', 'left', 'right']
|
||||
|
||||
class MovementManager {
|
||||
// Server auth movement : we send inputs, server calculates position & sends back
|
||||
serverMovements = false
|
||||
|
||||
constructor (bot) {
|
||||
this.bot = bot
|
||||
this.world = bot.world
|
||||
// Physics tick
|
||||
this.tick = 0n
|
||||
}
|
||||
|
||||
get lastPos () { return this.player.entity.position.clone() }
|
||||
set lastPos (newPos) { this.player.entity.position.set(newPos.x, newPos.y, newPos.z) }
|
||||
get lastRot () { return vec3(this.player.entity.yaw, this.player.entity.pitch, this.player.entity.headYaw) }
|
||||
set lastRot (rot) {
|
||||
if (!isNaN(rot.x)) this.player.entity.yaw = rot.x
|
||||
if (!isNaN(rot.y)) this.player.entity.pitch = rot.y
|
||||
if (!isNaN(rot.z)) this.player.entity.headYaw = rot.z
|
||||
}
|
||||
|
||||
// Ask the server to be in a new position
|
||||
requestPosition (time, inputState) {
|
||||
const positionUpdated = !this.lastSentPos || !this.lastPos.equals(this.lastSentPos)
|
||||
const rotationUpdated = !this.lastSentRot || !this.lastRot.equals(this.lastSentRot)
|
||||
|
||||
if (positionUpdated || rotationUpdated) {
|
||||
this.lastSentPos = this.lastPos.clone()
|
||||
// console.log('We computed', this.lastPos)
|
||||
this.bot.updatePlayerCamera(2, this.lastSentPos, this.playerState.yaw, this.playerState.pitch || this.player.entity.pitch)
|
||||
if (this.serverMovements) {
|
||||
globalThis.movePayload = {
|
||||
pitch: r2d(this.player.entity.pitch),
|
||||
yaw: r2d(this.player.entity.yaw),
|
||||
position: {
|
||||
x: this.lastPos.x,
|
||||
y: this.lastPos.y + 1.62,
|
||||
z: this.lastPos.z
|
||||
},
|
||||
move_vector: { // Minecraft coords, N: Z+1, S: Z-1, W: X+1, E: X-1
|
||||
x: inputState.left ? 1 : (inputState.right ? -1 : 0),
|
||||
z: inputState.up ? 1 : (inputState.down ? -1 : 0)
|
||||
},
|
||||
head_yaw: r2d(this.player.entity.yaw),
|
||||
input_data: inputState,
|
||||
input_mode: 'mouse',
|
||||
play_mode: 'screen',
|
||||
tick: this.tick,
|
||||
delta: this.lastSentPos?.minus(this.lastPos) ?? { x: 0, y: 0, z: 0 }
|
||||
}
|
||||
this.bot.client.queue('player_auth_input', globalThis.movePayload)
|
||||
}
|
||||
|
||||
this.positionUpdated = false
|
||||
this.lastSentPos = this.lastPos
|
||||
this.lastSentRot = this.lastRot
|
||||
}
|
||||
}
|
||||
|
||||
init (movementAuthority, position, velocity, yaw = 0, pitch = 0, headYaw = 0) {
|
||||
if (movementAuthority.includes('server')) {
|
||||
this.serverMovements = true
|
||||
}
|
||||
this.player = {
|
||||
version: '1.16.1',
|
||||
inventory: {
|
||||
slots: []
|
||||
},
|
||||
entity: {
|
||||
effects: {},
|
||||
position: vec3(position),
|
||||
velocity: vec3(velocity),
|
||||
onGround: false,
|
||||
isInWater: false,
|
||||
isInLava: false,
|
||||
isInWeb: false,
|
||||
isCollidedHorizontally: false,
|
||||
isCollidedVertically: false,
|
||||
yaw,
|
||||
pitch,
|
||||
headYaw // bedrock
|
||||
},
|
||||
events: { // Control events to send next tick
|
||||
startSprint: false,
|
||||
stopSprint: false,
|
||||
startSneak: false,
|
||||
stopSneak: false
|
||||
},
|
||||
sprinting: false,
|
||||
jumpTicks: 0,
|
||||
jumpQueued: false,
|
||||
downJump: false
|
||||
}
|
||||
|
||||
const mcData = require('minecraft-data')('1.16.1')
|
||||
this.physics = Physics(mcData, this.world)
|
||||
this.controls = {
|
||||
forward: false,
|
||||
back: false,
|
||||
left: false,
|
||||
right: false,
|
||||
jump: false,
|
||||
sprint: false,
|
||||
sneak: false
|
||||
}
|
||||
}
|
||||
|
||||
// This function should be executed each tick (every 0.05 seconds)
|
||||
// How it works: https://gafferongames.com/post/fix_your_timestep/
|
||||
timeAccumulator = 0
|
||||
lastPhysicsFrameTime = null
|
||||
inputQueue = []
|
||||
doPhysics () {
|
||||
const now = performance.now()
|
||||
const deltaSeconds = (now - this.lastPhysicsFrameTime) / 1000
|
||||
this.lastPhysicsFrameTime = now
|
||||
|
||||
this.timeAccumulator += deltaSeconds
|
||||
|
||||
while (this.timeAccumulator >= PHYSICS_TIMESTEP) {
|
||||
const q = this.inputQueue.shift()
|
||||
if (q) {
|
||||
Object.assign(this.playerState.control, q)
|
||||
if (!isNaN(q.yaw)) this.player.entity.yaw = q.yaw
|
||||
if (!isNaN(q.pitch)) this.player.entity.pitch = q.pitch
|
||||
}
|
||||
this.playerState = new PlayerState(this.player, this.controls)
|
||||
this.physics.simulatePlayer(this.playerState, this.world.sync).apply(this.player)
|
||||
this.lastPos = this.playerState.pos
|
||||
this.requestPosition(PHYSICS_TIMESTEP, {
|
||||
ascend: false,
|
||||
descend: false,
|
||||
// Players bob up and down in water, north jump is true when going up.
|
||||
// In water this is only true after the player has reached max height before bobbing back down.
|
||||
north_jump: this.player.jumpTicks > 0, // Jump
|
||||
jump_down: this.controls.jump, // Jump
|
||||
sprint_down: this.controls.sprint,
|
||||
change_height: false,
|
||||
jumping: this.controls.jump, // Jump
|
||||
auto_jumping_in_water: false,
|
||||
sneaking: false,
|
||||
sneak_down: false,
|
||||
up: this.controls.forward,
|
||||
down: this.controls.back,
|
||||
left: this.controls.right,
|
||||
right: this.controls.left,
|
||||
up_left: false,
|
||||
up_right: false,
|
||||
want_up: this.controls.jump, // Jump
|
||||
want_down: false,
|
||||
want_down_slow: false,
|
||||
want_up_slow: false,
|
||||
sprinting: false,
|
||||
ascend_scaffolding: false,
|
||||
descend_scaffolding: false,
|
||||
sneak_toggle_down: false,
|
||||
persist_sneak: false,
|
||||
start_sprinting: this.player.events.startSprint || false,
|
||||
stop_sprinting: this.player.events.stopSprint || false,
|
||||
start_sneaking: this.player.events.startSneak || false,
|
||||
stop_sneaking: this.player.events.stopSneak || false,
|
||||
// Player is Update Aqatic swimming
|
||||
start_swimming: false,
|
||||
// Player stops Update Aqatic swimming
|
||||
stop_swimming: false,
|
||||
start_jumping: this.player.jumpTicks === 1, // Jump
|
||||
start_gliding: false,
|
||||
stop_gliding: false
|
||||
})
|
||||
this.timeAccumulator -= PHYSICS_TIMESTEP
|
||||
this.tick++
|
||||
}
|
||||
}
|
||||
|
||||
startPhys () {
|
||||
console.log('Start phys')
|
||||
this.physicsLoop = setInterval(() => {
|
||||
this.doPhysics()
|
||||
}, PHYSICS_INTERVAL_MS)
|
||||
}
|
||||
|
||||
get sprinting() {
|
||||
return this.player.sprinting
|
||||
}
|
||||
|
||||
set sprinting(val) {
|
||||
this.player.events.startSprint = val
|
||||
this.player.events.stopSprint = !val
|
||||
if (val && !this.player.sprinting) {
|
||||
this.bot.emit('startSprint')
|
||||
} else {
|
||||
this.bot.emit('stopSprint')
|
||||
}
|
||||
this.player.sprinting = val
|
||||
}
|
||||
|
||||
_lastInput = { control: '', time: 0 }
|
||||
|
||||
/**
|
||||
* Sets the active control state and also keeps track of key toggles.
|
||||
* @param {'forward' | 'back' | 'left' | 'right' | 'jump' | 'sprint' | 'sneak'} control
|
||||
* @param {boolean} state
|
||||
*/
|
||||
setControlState (control, state, time = Date.now()) {
|
||||
// HACK ! switch left and right, fixes control issue
|
||||
if (control === 'left') control = 'right'
|
||||
else if (control === 'right') control = 'left'
|
||||
|
||||
if (this.controls[control] === state) return
|
||||
|
||||
const isAxis = AXES.includes(control)
|
||||
let hasOtherAxisKeyDown = false
|
||||
for (const c of AXES) {
|
||||
if (this.controls[c] && c != control) {
|
||||
hasOtherAxisKeyDown = true
|
||||
}
|
||||
}
|
||||
|
||||
if (control === 'sprint') {
|
||||
if (state && hasOtherAxisKeyDown) { // sprint down + a axis movement key
|
||||
this.sprinting = true
|
||||
} else if ((!state || !hasOtherAxisKeyDown) && this.sprinting) { // sprint up or movement key up & current sprinting
|
||||
this.bot.emit('stopSprint')
|
||||
this.sprinting = false
|
||||
}
|
||||
} else if (isAxis && this.controls.sprint) {
|
||||
if (!state && !hasOtherAxisKeyDown) {
|
||||
this.sprinting = false
|
||||
} else if (state && !hasOtherAxisKeyDown) {
|
||||
this.sprinting = true
|
||||
}
|
||||
} else if (control === 'sneak') {
|
||||
if (state) {
|
||||
this.player.events.startSneak = true
|
||||
this.bot.emit('startSneak')
|
||||
} else {
|
||||
this.player.events.stopSneak = true
|
||||
this.bot.emit('stopSneak')
|
||||
}
|
||||
} else if (control === 'forward' && this._lastInput.control === 'forward' && (Date.now() - this._lastInput.time) < 100 && !this.controls.sprint) {
|
||||
// double tap forward within 0.5 seconds, toggle sprint
|
||||
// this.controls.sprint = true
|
||||
// this.sprinting = true
|
||||
}
|
||||
|
||||
this._lastInput = { control, time }
|
||||
this.controls[control] = state
|
||||
}
|
||||
|
||||
stopPhys () {
|
||||
clearInterval(this.physicsLoop)
|
||||
}
|
||||
|
||||
// Called when a proxy player sends a PlayerInputPacket. We need to apply these inputs tick-by-tick
|
||||
// as these packets are sent by the client every tick.
|
||||
pushInputState (state, yaw, pitch) {
|
||||
const yawRad = d2r(yaw)
|
||||
const pitchRad = d2r(pitch)
|
||||
this.inputQueue.push({
|
||||
forward: state.up,
|
||||
back: state.down, // TODO: left and right switched ???
|
||||
left: state.right,
|
||||
right: state.left,
|
||||
jump: state.jump_down,
|
||||
sneak: state.sneak_down,
|
||||
yaw: yawRad,
|
||||
pitch: pitchRad
|
||||
})
|
||||
// debug
|
||||
globalThis.debugYaw = [yaw, yawRad]
|
||||
}
|
||||
|
||||
|
||||
// Called when a proxy player sends a PlayerInputPacket. We need to apply these inputs tick-by-tick
|
||||
// as these packets are sent by the client every tick.
|
||||
pushCameraControl (state, id = 1) {
|
||||
let { x, y, z } = state.position
|
||||
if (id === 1) y -= 1.62 // account for player bb
|
||||
const adjPos = vec3({ x, y, z })
|
||||
// Sneak resyncs the position for easy testing
|
||||
this.bot.updatePlayerCamera(id, adjPos, d2r(state.yaw), d2r(state.pitch), state.input_data.sneak_down)
|
||||
}
|
||||
|
||||
// Server gives us a new position
|
||||
updatePosition (pos, yaw, pitch, headYaw, tick) {
|
||||
this.lastPos = pos
|
||||
this.lastRot = { x: yaw, y: pitch, z: headYaw }
|
||||
if (tick) this.tick = tick
|
||||
}
|
||||
|
||||
// User has moved the camera. Update the movements stored.
|
||||
onViewerCameraMove (newYaw, newPitch, newHeadYaw) {
|
||||
this.lastRot = { x: newYaw, y: newPitch, z: newHeadYaw }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MovementManager }
|
||||
9
examples/viewer/client/preload.js
Normal file
9
examples/viewer/client/preload.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Required to detect electron in prismarine-viewer
|
||||
globalThis.isElectron = true
|
||||
|
||||
// If you need to disable node integration:
|
||||
// * Node.js APIs will only be avaliable in this file
|
||||
// * Use this file to load a viewer manager class
|
||||
// based on one of the examples
|
||||
// * Expose this class to the global window
|
||||
// * Interact with the class in your code
|
||||
22
examples/viewer/client/util.js
Normal file
22
examples/viewer/client/util.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const difference = (o1, o2) => Object.keys(o2).reduce((diff, key) => {
|
||||
if (o1[key] === o2[key]) return diff
|
||||
return {
|
||||
...diff,
|
||||
[key]: o2[key]
|
||||
}
|
||||
}, {})
|
||||
|
||||
const diff = (o1, o2) => { const dif = difference(o1, o2); return Object.keys(dif).length ? dif : null }
|
||||
|
||||
const d2r = deg => (180 - (deg < 0 ? (360 + deg) : deg)) * (Math.PI / 180)
|
||||
const r2d = rad => {
|
||||
let deg = rad * (180 / Math.PI)
|
||||
deg = deg % 360
|
||||
return 180 - deg
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
diff,
|
||||
d2r,
|
||||
r2d
|
||||
}
|
||||
2
examples/viewer/client/worker.js
Normal file
2
examples/viewer/client/worker.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// hack for path resolving
|
||||
require('prismarine-viewer/viewer/lib/worker')
|
||||
44
examples/viewer/index.js
Normal file
44
examples/viewer/index.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const path = require('path')
|
||||
const { app, BrowserWindow, globalShortcut } = require('electron')
|
||||
|
||||
function createMainWindow() {
|
||||
const window = new BrowserWindow({
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, './client/preload.js')
|
||||
}
|
||||
})
|
||||
|
||||
// Open dev tools on load
|
||||
window.webContents.openDevTools()
|
||||
|
||||
window.loadFile(path.join(__dirname, './client/index.html'))
|
||||
|
||||
window.webContents.on('devtools-opened', () => {
|
||||
window.focus()
|
||||
setImmediate(() => {
|
||||
window.focus()
|
||||
})
|
||||
})
|
||||
|
||||
return window
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
const win = createMainWindow()
|
||||
|
||||
globalShortcut.register('CommandOrControl+W', () => {
|
||||
win.webContents.sendInputEvent({
|
||||
type: 'keyDown',
|
||||
keyCode: 'W'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
app.quit()
|
||||
})
|
||||
|
||||
app.allowRendererProcessReuse = false
|
||||
15
examples/viewer/package.json
Normal file
15
examples/viewer/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "bedrock-protocol-viewer",
|
||||
"description": "bedrock-protocol prismarine-viewer example",
|
||||
"scripts": {
|
||||
"start": "electron ."
|
||||
},
|
||||
"dependencies": {
|
||||
"bedrock-protocol": "file:../../",
|
||||
"browserify-cipher": "^1.0.1",
|
||||
"electron": "^12.0.2",
|
||||
"patch-package": "^6.4.7",
|
||||
"prismarine-physics": "^1.2.2",
|
||||
"prismarine-viewer": "^1.19.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -23,18 +23,19 @@
|
|||
"@azure/msal-node": "^1.0.0-beta.6",
|
||||
"@jsprismarine/jsbinaryutils": "^2.1.8",
|
||||
"@xboxreplay/xboxlive-auth": "^3.3.3",
|
||||
"aes-js": "^3.1.2",
|
||||
"asn1": "^0.2.4",
|
||||
"bedrock-provider": "^0.1.1",
|
||||
"browserify-cipher": "^1.0.1",
|
||||
"bedrock-provider": "^1.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"ec-pem": "^0.18.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsp-raknet": "github:extremeheat/raknet#client",
|
||||
"leveldb-zlib": "0.0.26",
|
||||
"minecraft-folder-path": "^1.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prismarine-nbt": "^1.5.0",
|
||||
"protodef": "^1.11.0",
|
||||
"raknet-native": "^0.2.0",
|
||||
"raknet-native": "^1.0.0",
|
||||
"uuid-1345": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ const JWT = require('jsonwebtoken')
|
|||
const DataProvider = require('../../data/provider')
|
||||
const ecPem = require('ec-pem')
|
||||
const curve = 'secp384r1'
|
||||
const { nextUUID } = require('../datatypes/util')
|
||||
|
||||
module.exports = (client, server, options) => {
|
||||
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
|
||||
|
|
@ -45,10 +46,10 @@ module.exports = (client, server, options) => {
|
|||
CapeImageHeight: 0,
|
||||
CapeImageWidth: 0,
|
||||
CapeOnClassicSkin: false,
|
||||
ClientRandomId: 1, // TODO make biggeer
|
||||
ClientRandomId: Date.now(),
|
||||
CurrentInputMode: 1,
|
||||
DefaultInputMode: 1,
|
||||
DeviceId: '2099de18-429a-465a-a49b-fc4710a17bb3', // TODO random
|
||||
DeviceId: nextUUID(),
|
||||
DeviceModel: '',
|
||||
DeviceOS: client.session?.deviceOS || 7,
|
||||
GameVersion: options.version || '1.16.201',
|
||||
|
|
@ -64,7 +65,7 @@ module.exports = (client, server, options) => {
|
|||
// inside of PlayFab.
|
||||
PlayFabId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0', // 1.16.210
|
||||
PremiumSkin: false,
|
||||
SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343701',
|
||||
SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343702',
|
||||
ServerAddress: `${options.hostname}:${options.port}`,
|
||||
SkinAnimationData: '',
|
||||
SkinColor: '#ffffcd96',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const JWT = require('jsonwebtoken')
|
||||
const constants = require('./constants')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
module.exports = (client, server, options) => {
|
||||
// Refer to the docs:
|
||||
|
|
@ -19,14 +20,14 @@ module.exports = (client, server, options) => {
|
|||
let finalKey = null
|
||||
// console.log(pubKey)
|
||||
for (const token of chain) {
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] })
|
||||
// console.log('Decoded', decoded)
|
||||
|
||||
// Check if signed by Mojang key
|
||||
const x5u = getX5U(token)
|
||||
if (x5u === constants.PUBLIC_KEY && !data.extraData?.XUID) {
|
||||
// didVerify = true
|
||||
console.log('verified with mojang key!', x5u)
|
||||
debug('Verified client with mojang key', x5u)
|
||||
}
|
||||
|
||||
// TODO: Handle `didVerify` = false
|
||||
|
|
@ -41,7 +42,7 @@ module.exports = (client, server, options) => {
|
|||
|
||||
function verifySkin (publicKey, token) {
|
||||
const pubKey = mcPubKeyToPem(publicKey)
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] })
|
||||
return decoded
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ class MinecraftTokenManager {
|
|||
const token = this.cache.mca
|
||||
debug('[mc] token cache', this.cache)
|
||||
if (!token) return
|
||||
console.log('TOKEN', token)
|
||||
debug('Auth token', token)
|
||||
const jwt = token.chain[0]
|
||||
const [header, payload, signature] = jwt.split('.').map(k => Buffer.from(k, 'base64')) // eslint-disable-line
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,19 @@ const ClientStatus = {
|
|||
}
|
||||
|
||||
class Connection extends EventEmitter {
|
||||
status = ClientStatus.Disconnected
|
||||
#status = ClientStatus.Disconnected
|
||||
q = []
|
||||
q2 = []
|
||||
|
||||
get status () {
|
||||
return this.#status
|
||||
}
|
||||
|
||||
set status (val) {
|
||||
debug('* new status', val)
|
||||
this.#status = val
|
||||
}
|
||||
|
||||
versionLessThan (version) {
|
||||
if (typeof version === 'string') {
|
||||
return Versions[version] < this.options.protocolVersion
|
||||
|
|
@ -117,7 +126,7 @@ class Connection extends EventEmitter {
|
|||
|
||||
sendEncryptedBatch (batch) {
|
||||
const buf = batch.stream.getBuffer()
|
||||
debug('Sending encrypted batch', batch)
|
||||
// debug('Sending encrypted batch', batch)
|
||||
this.encrypt(buf)
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +141,7 @@ class Connection extends EventEmitter {
|
|||
this.outLog('Enc buf', buf)
|
||||
const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header
|
||||
|
||||
this.outLog('Sending wrapped encrypted batch', packet)
|
||||
// this.outLog('Sending wrapped encrypted batch', packet)
|
||||
this.sendMCPE(packet)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,10 +38,8 @@ class BatchPacket {
|
|||
|
||||
encode () {
|
||||
const buf = this.stream.getBuffer()
|
||||
console.log('Encoding payload', buf)
|
||||
const def = Zlib.deflateRawSync(buf, { level: this.compressionLevel })
|
||||
const ret = Buffer.concat([Buffer.from([0xfe]), def])
|
||||
console.log('Compressed', ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,4 +39,8 @@ function uuidFrom (string) {
|
|||
return UUID.v3({ namespace: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', name: string })
|
||||
}
|
||||
|
||||
module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom }
|
||||
function nextUUID () {
|
||||
return uuidFrom(Date.now().toString())
|
||||
}
|
||||
|
||||
module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom, nextUUID }
|
||||
|
|
|
|||
18
src/rak.js
18
src/rak.js
|
|
@ -5,8 +5,9 @@ const Reliability = require('jsp-raknet/protocol/reliability')
|
|||
const RakClient = require('jsp-raknet/client')
|
||||
const ConnWorker = require('./rakWorker')
|
||||
const { waitFor } = require('./datatypes/util')
|
||||
const ServerName = require('./server/advertisement')
|
||||
try {
|
||||
var { Client, Server, PacketPriority, PacketReliability, McPingMessage } = require('raknet-native') // eslint-disable-line
|
||||
var { Client, Server, PacketPriority, PacketReliability } = require('raknet-native') // eslint-disable-line
|
||||
} catch (e) {
|
||||
console.debug('[raknet] native not found, using js', e)
|
||||
}
|
||||
|
|
@ -19,16 +20,17 @@ class RakNativeClient extends EventEmitter {
|
|||
this.onCloseConnection = () => { }
|
||||
this.onEncapsulated = () => { }
|
||||
|
||||
this.raknet = new Client(options.hostname, options.port, 'minecraft')
|
||||
this.raknet = new Client(options.hostname, options.port, { protocolVersion: 10 })
|
||||
this.raknet.on('encapsulated', ({ buffer, address }) => {
|
||||
this.onEncapsulated(buffer, address)
|
||||
})
|
||||
this.raknet.on('connected', () => {
|
||||
|
||||
this.raknet.on('connect', () => {
|
||||
this.connected = true
|
||||
this.onConnected()
|
||||
})
|
||||
|
||||
this.raknet.on('disconnected', ({ reason }) => {
|
||||
this.raknet.on('disconnect', ({ reason }) => {
|
||||
this.connected = false
|
||||
this.onCloseConnection(reason)
|
||||
})
|
||||
|
|
@ -64,16 +66,17 @@ class RakNativeClient extends EventEmitter {
|
|||
}
|
||||
|
||||
class RakNativeServer extends EventEmitter {
|
||||
constructor (options = {}) {
|
||||
constructor (options = {}, server) {
|
||||
super()
|
||||
this.onOpenConnection = () => { }
|
||||
this.onCloseConnection = () => { }
|
||||
this.onEncapsulated = () => { }
|
||||
this.raknet = new Server(options.hostname, options.port, {
|
||||
maxConnections: options.maxConnections || 3,
|
||||
minecraft: {},
|
||||
message: new McPingMessage().toBuffer()
|
||||
protocolVersion: 10,
|
||||
message: ServerName.getServerName(server)
|
||||
})
|
||||
// TODO: periodically update the server name until we're closed
|
||||
|
||||
this.raknet.on('openConnection', (client) => {
|
||||
client.sendReliable = function (buffer, immediate) {
|
||||
|
|
@ -90,7 +93,6 @@ class RakNativeServer extends EventEmitter {
|
|||
})
|
||||
|
||||
this.raknet.on('encapsulated', ({ buffer, address }) => {
|
||||
// console.log('ENCAP',thingy)
|
||||
this.onEncapsulated(buffer, address)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
25
src/relay.js
25
src/relay.js
|
|
@ -2,8 +2,8 @@
|
|||
const { Client } = require('./client')
|
||||
const { Server } = require('./server')
|
||||
const { Player } = require('./serverPlayer')
|
||||
const debug = require('debug')('minecraft-protocol relay')
|
||||
const { serialize } = require('./datatypes/util')
|
||||
const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol relay')
|
||||
// const { serialize } = require('./datatypes/util')
|
||||
|
||||
/** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */
|
||||
|
||||
|
|
@ -22,10 +22,10 @@ class RelayPlayer extends Player {
|
|||
})
|
||||
this.downQ = []
|
||||
this.upQ = []
|
||||
this.upInLog = (...msg) => console.info('** Backend -> Proxy', ...msg)
|
||||
this.upOutLog = (...msg) => console.info('** Proxy -> Backend', ...msg)
|
||||
this.downInLog = (...msg) => console.info('** Client -> Proxy', ...msg)
|
||||
this.downOutLog = (...msg) => console.info('** Proxy -> Client', ...msg)
|
||||
this.upInLog = (...msg) => console.debug('** Backend -> Proxy', ...msg)
|
||||
this.upOutLog = (...msg) => console.debug('** Proxy -> Backend', ...msg)
|
||||
this.downInLog = (...msg) => console.debug('** Client -> Proxy', ...msg)
|
||||
this.downOutLog = (...msg) => console.debug('** Proxy -> Client', ...msg)
|
||||
|
||||
if (!server.options.logging) {
|
||||
this.upInLog = () => { }
|
||||
|
|
@ -52,11 +52,11 @@ class RelayPlayer extends Player {
|
|||
this.downQ.push(packet)
|
||||
return
|
||||
}
|
||||
this.upInLog('Recv packet', packet)
|
||||
// this.upInLog('Recv packet', packet)
|
||||
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('~ 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
|
||||
|
||||
|
|
@ -72,6 +72,8 @@ class RelayPlayer extends Player {
|
|||
|
||||
this.queue(name, params)
|
||||
// this.sendBuffer(packet)
|
||||
|
||||
this.emit('clientbound', des.data)
|
||||
}
|
||||
|
||||
// Send queued packets to the connected client
|
||||
|
|
@ -105,7 +107,7 @@ class RelayPlayer extends Player {
|
|||
return
|
||||
}
|
||||
this.flushUpQueue() // Send queued packets
|
||||
this.downInLog('Recv packet', packet)
|
||||
// this.downInLog('Recv packet', packet)
|
||||
// TODO: If we fail to parse a packet, proxy it raw and log an error
|
||||
const des = this.server.deserializer.parsePacketBuffer(packet)
|
||||
|
||||
|
|
@ -129,6 +131,7 @@ class RelayPlayer extends Player {
|
|||
this.downInLog('Relaying', des.data)
|
||||
this.upstream.sendBuffer(packet)
|
||||
}
|
||||
this.emit('serverbound', des.data)
|
||||
} else {
|
||||
super.readPacket(packet)
|
||||
}
|
||||
|
|
@ -162,6 +165,8 @@ class Relay extends Server {
|
|||
ds.flushUpQueue()
|
||||
console.log('Connected to upstream server')
|
||||
client.readPacket = (packet) => ds.readUpstream(packet)
|
||||
|
||||
this.emit('join', /* client connected to proxy */ ds, /* backend server */ client)
|
||||
})
|
||||
this.upstreams.set(clientAddr.hash, client)
|
||||
}
|
||||
|
|
@ -183,7 +188,7 @@ class Relay extends Server {
|
|||
const player = new this.RelayPlayer(this, conn)
|
||||
console.debug('New connection from', conn.address)
|
||||
this.clients[conn.address] = player
|
||||
this.emit('connect', { client: player })
|
||||
this.emit('connect', player)
|
||||
this.openUpstreamConnection(player, conn.address)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const { createDeserializer, createSerializer } = require('./transforms/serialize
|
|||
const { Player } = require('./serverPlayer')
|
||||
const { RakServer } = require('./rak')
|
||||
const Options = require('./options')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol')
|
||||
|
||||
class Server extends EventEmitter {
|
||||
constructor (options) {
|
||||
|
|
@ -62,6 +62,7 @@ class Server extends EventEmitter {
|
|||
this.raknet.onOpenConnection = this.onOpenConnection
|
||||
this.raknet.onCloseConnection = this.onCloseConnection
|
||||
this.raknet.onEncapsulated = this.onEncapsulated
|
||||
return { hostname, port }
|
||||
}
|
||||
|
||||
close (disconnectReason) {
|
||||
|
|
|
|||
39
src/server/advertisement.js
Normal file
39
src/server/advertisement.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
class ServerName {
|
||||
motd = 'Bedrock Protocol Server'
|
||||
name = 'bedrock-protocol'
|
||||
protocol = 408
|
||||
version = '1.16.20'
|
||||
players = {
|
||||
online: 0,
|
||||
max: 5
|
||||
}
|
||||
|
||||
gamemode = 'Creative'
|
||||
serverId = '0'
|
||||
|
||||
toString (version) {
|
||||
return [
|
||||
'MCPE',
|
||||
this.motd,
|
||||
this.protocol,
|
||||
this.version,
|
||||
this.players.online,
|
||||
this.players.max,
|
||||
this.serverId,
|
||||
this.name,
|
||||
this.gamemode
|
||||
].join(';') + ';'
|
||||
}
|
||||
|
||||
toBuffer (version) {
|
||||
const str = this.toString(version)
|
||||
return Buffer.concat([Buffer.from([0, str.length]), Buffer.from(str)])
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ServerName,
|
||||
getServerName (client) {
|
||||
return new ServerName().toBuffer()
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ class Player extends Connection {
|
|||
// TODO: disconnect user
|
||||
throw new Error('Failed to verify user')
|
||||
}
|
||||
console.log('Verified user', 'got pub key', key, userData)
|
||||
debug('Verified user pub key', key, userData)
|
||||
|
||||
this.emit('login', { user: userData.extraData }) // emit events for user
|
||||
this.emit('server.client_handshake', { key }) // internal so we start encryption
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const { Transform } = require('readable-stream')
|
||||
const crypto = require('crypto')
|
||||
const aesjs = require('aes-js')
|
||||
const Zlib = require('zlib')
|
||||
if (globalThis.isElectron) var { CipherCFB8 } = require('raknet-native') // eslint-disable-line
|
||||
|
||||
const CIPHER_ALG = 'aes-256-cfb8'
|
||||
|
||||
|
|
@ -22,32 +22,23 @@ function createDecipher (secret, initialValue) {
|
|||
class Cipher extends Transform {
|
||||
constructor (secret, iv) {
|
||||
super()
|
||||
this.aes = new aesjs.ModeOfOperation.cfb(secret, iv, 1) // eslint-disable-line new-cap
|
||||
this.aes = new CipherCFB8(secret, iv)
|
||||
}
|
||||
|
||||
_transform (chunk, enc, cb) {
|
||||
try {
|
||||
const res = this.aes.encrypt(chunk)
|
||||
cb(null, res)
|
||||
} catch (e) {
|
||||
cb(e)
|
||||
}
|
||||
const ciphered = this.aes.cipher(chunk)
|
||||
cb(null, ciphered)
|
||||
}
|
||||
}
|
||||
|
||||
class Decipher extends Transform {
|
||||
constructor (secret, iv) {
|
||||
super()
|
||||
this.aes = new aesjs.ModeOfOperation.cfb(secret, iv, 1) // eslint-disable-line new-cap
|
||||
this.aes = new CipherCFB8(secret, iv)
|
||||
}
|
||||
|
||||
_transform (chunk, enc, cb) {
|
||||
try {
|
||||
const res = this.aes.decrypt(chunk)
|
||||
cb(null, res)
|
||||
} catch (e) {
|
||||
cb(e)
|
||||
}
|
||||
cb(null, this.aes.decipher(chunk))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,10 +61,12 @@ function createEncryptor (client, iv) {
|
|||
// The send counter is represented as a little-endian 64-bit long and incremented after each packet.
|
||||
|
||||
function process (chunk) {
|
||||
const buffer = Zlib.deflateRawSync(chunk, { level: 7 })
|
||||
const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)])
|
||||
client.sendCounter++
|
||||
client.cipher.write(packet)
|
||||
Zlib.deflateRaw(chunk, { level: 7 }, (err, buffer) => {
|
||||
if (err) throw err
|
||||
const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)])
|
||||
client.sendCounter++
|
||||
client.cipher.write(packet)
|
||||
})
|
||||
}
|
||||
|
||||
client.cipher.on('data', client.onEncryptedPacket)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ async function startServerAndWait (version, withTimeout, options) {
|
|||
|
||||
if (!module.parent) {
|
||||
// if (process.argv.length < 3) throw Error('Missing version argument')
|
||||
startServer(process.argv[2] || '1.16.201')
|
||||
startServer(process.argv[2] || '1.16.201', null, process.argv[3] ? { 'server-port': process.argv[3] } : undefined)
|
||||
}
|
||||
|
||||
module.exports = { fetchLatestStable, startServer, startServerAndWait }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue