├── g_python ├── __init__.py ├── hdirection.py ├── hmessage.py ├── hunitytools.py ├── hunityparsers.py ├── gbot.py ├── htools.py ├── hpacket.py ├── hparsers.py └── gextension.py ├── .gitattributes ├── tests ├── user_profile.py ├── height_map.py ├── read_long.py ├── console_bot.py ├── packet_logger.py ├── room_stuff_unity.py ├── inventory_items.py ├── extension_interface.py ├── room_stuff.py ├── extension_console.py └── packets_example.py ├── setup.py ├── LICENSE ├── .gitignore └── README.md /g_python/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /g_python/hdirection.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class Direction(IntEnum): 5 | TO_CLIENT = 0 6 | TO_SERVER = 1 7 | -------------------------------------------------------------------------------- /tests/user_profile.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python import hparsers 4 | from g_python.gextension import Extension 5 | from g_python.hmessage import Direction 6 | 7 | extension_info = { 8 | "title": "User profile", 9 | "description": "g_python test", 10 | "version": "1.0", 11 | "author": "sirjonasxx" 12 | } 13 | 14 | ext = Extension(extension_info, sys.argv) 15 | ext.start() 16 | 17 | 18 | def user_profile(message): 19 | profile = hparsers.HUserProfile(message.packet) 20 | print(profile) 21 | 22 | 23 | ext.intercept(Direction.TO_CLIENT, user_profile, 'ExtendedProfile') 24 | -------------------------------------------------------------------------------- /tests/height_map.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gextension import Extension 4 | from g_python.hmessage import Direction 5 | from g_python.hparsers import HHeightMap 6 | 7 | extension_info = { 8 | "title": "Heightmap", 9 | "description": "just an example", 10 | "version": "1.0", 11 | "author": "WiredSPast" 12 | } 13 | 14 | ext = Extension(extension_info, sys.argv) 15 | ext.start() 16 | 17 | 18 | def on_height_map(msg): 19 | heightmap = HHeightMap(msg.packet) 20 | print(heightmap.get_tile(10, 10)) 21 | 22 | 23 | ext.intercept(Direction.TO_CLIENT, on_height_map, 'HeightMap') 24 | -------------------------------------------------------------------------------- /tests/read_long.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gextension import Extension 4 | from g_python.hmessage import Direction 5 | 6 | extension_info = { 7 | "title": "long example LOL", 8 | "description": "g_python test", 9 | "version": "1.0", 10 | "author": "Hellsin6" 11 | } 12 | 13 | ext = Extension(extension_info, sys.argv) 14 | ext.start() 15 | 16 | 17 | def StuffDataUpdate(message): 18 | (x, y, z) = message.packet.read('lis') 19 | print('StuffDataUpdate', x, y, z) 20 | 21 | furni_id = message.packet.read_long(6) 22 | print('read_long', furni_id) 23 | 24 | 25 | ext.intercept(Direction.TO_CLIENT, StuffDataUpdate, 88) -------------------------------------------------------------------------------- /tests/console_bot.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gbot import ConsoleBot, HBotProfile 4 | from g_python.gextension import Extension 5 | 6 | extension_info = { 7 | "title": "Console Bot", 8 | "description": "", 9 | "version": "1.0", 10 | "author": "b@u@o", 11 | } 12 | 13 | ext = Extension(extension_info, sys.argv) 14 | ext.start() 15 | 16 | 17 | def ping(): 18 | bot.send("Pong!") 19 | 20 | 21 | # you don't need to initialize settings, just if you want to customize 22 | settings: HBotProfile = { 23 | "username": "Custom Bot", 24 | "motto": "Custom Motto" 25 | } 26 | 27 | bot = ConsoleBot(ext, prefix=":", bot_settings=settings) 28 | bot.add_command("ping", ping) 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="g-python", 8 | version="0.1.6", 9 | author="sirjonasxx", 10 | author_email="sirjonasxx@hotmail.com", 11 | description="G-Earth extension interface for Python.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/sirjonasxx/G-Python", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires='>=3.5', 22 | ) 23 | -------------------------------------------------------------------------------- /tests/packet_logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gextension import Extension 4 | from g_python.hmessage import Direction 5 | 6 | extension_info = { 7 | "title": "Packet Logger", 8 | "description": "g_python test", 9 | "version": "1.0", 10 | "author": "sirjonasxx" 11 | } 12 | 13 | ext = Extension(extension_info, sys.argv) 14 | ext.start() 15 | 16 | 17 | def all_packets(message): 18 | packet = message.packet 19 | s = packet.g_string(ext) 20 | expr = packet.g_expression(ext) 21 | print('{} --> {}'.format(message.direction.name, s)) 22 | if expr != '': 23 | print(expr) 24 | print('------------------------------------') 25 | 26 | 27 | ext.intercept(Direction.TO_CLIENT, all_packets) 28 | ext.intercept(Direction.TO_SERVER, all_packets) -------------------------------------------------------------------------------- /tests/room_stuff_unity.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gextension import Extension 4 | from g_python.hunitytools import UnityRoomUsers, UnityRoomFurni 5 | 6 | extension_info = { 7 | "title": "Room stuff unity", 8 | "description": "g_python test", 9 | "version": "1.2", 10 | "author": "sirjonasxx & Hellsin6" 11 | } 12 | 13 | ext = Extension(extension_info, sys.argv) 14 | ext.start() 15 | 16 | 17 | def print_furnis(furnis): 18 | for furni in furnis: 19 | print("furno", furni.id, furni.type_id, furni.tile, furni.owner) 20 | 21 | 22 | room_users = UnityRoomUsers(ext) 23 | room_users.on_new_users(lambda users: print(list(map(str, users)))) 24 | 25 | room_furni = UnityRoomFurni(ext) 26 | room_furni.on_floor_furni_load(lambda furnis: print_furnis(furnis)) 27 | -------------------------------------------------------------------------------- /tests/inventory_items.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gextension import Extension 4 | from g_python.htools import Inventory 5 | 6 | extension_info = { 7 | "title": "Inventory items", 8 | "description": "g_python test", 9 | "version": "1.0", 10 | "author": "sirjonasxx" 11 | } 12 | 13 | ext = Extension(extension_info, sys.argv, {"use_click_trigger": True}) 14 | ext.start() 15 | 16 | inv = Inventory(ext) 17 | # inventory items will be available under: 18 | # inv.inventory_items (list of HInventoryItem) 19 | 20 | 21 | def request_inventory(): 22 | print("Requesting inventory") 23 | inv.request() 24 | 25 | 26 | def on_inventory_load(items): 27 | print("Found {} items!".format(len(items))) 28 | 29 | 30 | ext.on_event('double_click', request_inventory) 31 | inv.on_inventory_load(on_inventory_load) 32 | -------------------------------------------------------------------------------- /tests/extension_interface.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gextension import Extension 4 | 5 | extension_info = { 6 | "title": "Extension stuff", 7 | "description": "g_python test", 8 | "version": "1.0", 9 | "author": "sirjonasxx" 10 | } 11 | 12 | extension_settings = { 13 | "use_click_trigger": True, 14 | "can_leave": True, 15 | "can_delete": True 16 | } 17 | 18 | ext = Extension(extension_info, sys.argv, extension_settings) 19 | 20 | 21 | def on_connection_start(): 22 | print('Connected with: {}:{}'.format(ext.connection_info['host'], ext.connection_info['port'])) 23 | print(ext.packet_infos) 24 | 25 | 26 | ext.on_event('double_click', lambda: print('Extension has been clicked')) 27 | ext.on_event('init', lambda: print('Initialized with g-earth')) 28 | ext.on_event('connection_start', on_connection_start) 29 | ext.on_event('connection_end', lambda: print('Connection ended')) 30 | 31 | ext.start() 32 | 33 | print(ext.request_flags()) 34 | -------------------------------------------------------------------------------- /tests/room_stuff.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | from g_python.gextension import Extension 5 | from g_python.htools import RoomFurni, RoomUsers 6 | 7 | extension_info = { 8 | "title": "Room stuff", 9 | "description": "g_python test", 10 | "version": "1.0", 11 | "author": "sirjonasxx" 12 | } 13 | 14 | ext = Extension(extension_info, sys.argv) 15 | ext.start() 16 | 17 | room_furni = RoomFurni(ext) 18 | room_furni.on_floor_furni_load(lambda furni: print("Found {} floor furniture in room".format(len(furni)))) 19 | room_furni.on_wall_furni_load(lambda furni: print("Found {} wall furniture in room".format(len(furni)))) 20 | 21 | room_users = RoomUsers(ext) 22 | room_users.on_new_users(lambda users: print(list(map(str, users)))) 23 | 24 | # current room users & furniture are always available under: 25 | # room_furni.floor_furni (list of HFloorItem) 26 | # room_furni.wall_furni (list of HWallItem) 27 | # room_users.room_users (list of HEntitity) 28 | 29 | # you can also request the users/furniture with the .request() method 30 | 31 | time.sleep(0.5) 32 | room_furni.request() -------------------------------------------------------------------------------- /tests/extension_console.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from g_python.gextension import Extension 4 | from g_python.hmessage import Direction 5 | 6 | extension_info = { 7 | "title": "Extension console", 8 | "description": "just an example", 9 | "version": "2.0", 10 | "author": "Lande" 11 | } 12 | 13 | ext = Extension(extension_info, sys.argv) 14 | ext.start() 15 | 16 | 17 | def speech_out(msg): 18 | text, bubble, id = msg.packet.read('sii') 19 | 20 | ext.write_to_console(f"Message send -> message : '{text}', bubble : {bubble}") 21 | 22 | 23 | def speech_in(msg): 24 | index, text, _, bubble, _, id = msg.packet.read('isiiii') 25 | 26 | ext.write_to_console(f"Message receive -> index : {index}, message : {text}, bubble : {bubble}", color='blue', mention_title=False) 27 | 28 | 29 | ext.intercept(Direction.TO_SERVER, speech_out, 'Chat') 30 | ext.intercept(Direction.TO_CLIENT, speech_in, 'Chat') 31 | 32 | ''' 33 | mention_title=True -> Show the name of the extension before the message ( True by default ) 34 | color='black' -> Color of the text ( Black by default ) 35 | ''' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 placeholder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /g_python/hmessage.py: -------------------------------------------------------------------------------- 1 | from typing import Self 2 | 3 | from .hpacket import HPacket 4 | from .hdirection import Direction 5 | 6 | 7 | class HMessage: 8 | def __init__(self, packet: HPacket, direction: Direction, index: int, is_blocked: bool = False): 9 | self.packet = packet 10 | self.direction = direction 11 | self._index = index 12 | self.is_blocked = is_blocked 13 | 14 | @classmethod 15 | def reconstruct_from_java(cls, string: str) -> Self: 16 | obj = cls.__new__(cls) 17 | super(HMessage, obj).__init__() 18 | 19 | split = string.split('\t', 3) 20 | obj.is_blocked = split[0] == '1' 21 | obj._index = int(split[1]) 22 | obj.direction = Direction.TO_CLIENT if split[2] == 'TOCLIENT' else Direction.TO_SERVER 23 | obj.packet = HPacket.reconstruct_from_java(split[3]) 24 | return obj 25 | 26 | def __repr__(self) -> str: 27 | return "{}\t{}\t{}\t{}".format( 28 | '1' if self.is_blocked else '0', 29 | self._index, 30 | 'TOCLIENT' if self.direction == Direction.TO_CLIENT else 'TOSERVER', 31 | repr(self.packet) 32 | ) 33 | 34 | def index(self) -> int: 35 | return self._index 36 | -------------------------------------------------------------------------------- /tests/packets_example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from time import sleep 3 | 4 | from g_python.gextension import Extension 5 | from g_python.hmessage import Direction, HMessage 6 | from g_python.hpacket import HPacket 7 | 8 | extension_info = { 9 | "title": "Packets example", 10 | "description": "g_python test", 11 | "version": "1.0", 12 | "author": "sirjonasxx" 13 | } 14 | 15 | ext = Extension(extension_info, sys.argv) 16 | ext.start() 17 | ext.send_to_server(HPacket("Chat", "hi", 1, 1)) 18 | 19 | def on_walk(message): 20 | # packet = message.packet 21 | # x = packet.read_int() 22 | # y = packet.read_int() 23 | (x, y) = message.packet.read('ii') 24 | print("Walking to x:{}, y={}".format(x, y)) 25 | 26 | # send packet to server from HPacket() object 27 | # ext.send_to_server(HPacket(1843, 1)) # wave 28 | ext.send_to_server(HPacket('AvatarExpression', 1)) # wave 29 | 30 | # # 2 ways of sending packets from string representations 31 | # ext.send_to_client('{l}{u:1411}{i:0}{s:"hi"}{i:0}{i:23}{i:0}{i:2}') 32 | # ext.send_to_client(HPacket.from_string('[0][0][0][26][5][131][0][0][0][0][0][2]ho[0][0][0][0][0][0][0][3][0][0][0][0][0][0][0][2]', ext)) 33 | 34 | 35 | # intercepted async, you can't modify it 36 | def on_speech(message): 37 | sleep(4) 38 | (text, color, index) = message.packet.read('sii') 39 | print("User said: {}, 4 seconds ago".format(text)) 40 | 41 | # intercepted async, but you can modify it 42 | def on_shout(message : HMessage): 43 | sleep(2) 44 | (text, color) = message.packet.read('si') 45 | message.is_blocked = (text == 'blocked') # block packet if speech equals "blocked" 46 | print("User shouted: {}, 2 seconds ago".format(text)) 47 | message.packet.replace_string(6, "G - " + text) 48 | 49 | 50 | ext.intercept(Direction.TO_SERVER, on_walk, 'MoveAvatar') 51 | ext.intercept(Direction.TO_SERVER, on_speech, 'Chat', mode='async') 52 | ext.intercept(Direction.TO_SERVER, on_shout, 'Shout', mode='async_modify') 53 | 54 | packet = HPacket(1231, "hi", 5, "old", False, True, "lol") 55 | result = packet.g_expression(ext) # get G-Earth's predicted expression for the packet above 56 | print(result) 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | private_tests/ 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | 129 | # intelliJ 130 | .idea/ 131 | *.iml 132 | out/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # G-Python 2 | G-Earth extension interface for Python. 3 | 4 | G-Earth + G-Python allows you to create simple scripts for Habbo and run them on the fly! 5 | 6 | ## Installation 7 | _Requires python >= 3.5: https://www.python.org/downloads/_ 8 | _Note: during Windows installation, make sure to select "Add python to PATH" if you want to install G-Python extensions in G-Earth_ 9 | ![image](https://user-images.githubusercontent.com/36828922/129458391-b10339e0-5671-4b8e-b644-da417730514f.png) 10 | 11 | 12 | Then execute the following in a terminal: 13 | `python -m pip install g-python` 14 | 15 | ## Features 16 | G-Python exports the following modules: 17 | 18 | ```python 19 | from g_python.gextension import Extension 20 | from g_python.hmessage import Direction, HMessage 21 | from g_python.hpacket import HPacket 22 | from g_python import hparsers 23 | from g_python import htools 24 | ``` 25 | 26 | * At any point where a `(header)id` is required, a `name` or `hash` can be used as well, if G-Earth is connected to Harble API 27 | * "hparsers" contains a load of useful parsers 28 | * "htools" contains fully prepared environments for accessing your Inventory, Room Furniture, and Room Users 29 | 30 | 31 | ## Usage 32 | 33 | Examples are available in the `tests/` folder. _(highly recommended to check out, since it contains functionality not shown underneath)_ 34 | 35 | This is a template extension with the minimal amount of code to connect with G-Earth: 36 | 37 | ```python 38 | import sys 39 | from g_python.gextension import Extension 40 | 41 | extension_info = { 42 | "title": "Extension stuff", 43 | "description": "g_python test", 44 | "version": "1.0", 45 | "author": "sirjonasxx" 46 | } 47 | 48 | ext = Extension(extension_info, sys.argv) # sys.argv are the commandline arguments, for example ['-p', '9092'] (G-Earth's extensions port) 49 | ext.start() 50 | ``` 51 | It is possible to register for events: 52 | ```python 53 | ext.on_event('double_click', lambda: print('Extension has been clicked')) 54 | ext.on_event('init', lambda: print('Initialized with g-earth')) 55 | ext.on_event('connection_start', lambda: print('Connection started')) 56 | ext.on_event('connection_end', lambda: print('Connection ended')) 57 | ``` 58 | Packet injection: 59 | ```python 60 | # sending packets to the server 61 | ext.send_to_server(HPacket('RoomUserAction', 1)) # wave using harble api name 62 | ext.send_to_server(HPacket(1843, 1)) # wave using header Id 63 | ext.send_to_server(HPacket('623058bd68a68267114aa8d1ee15b597', 1)) # wave using harble api hash 64 | 65 | # sending packets from raw text: 66 | ext.send_to_client('{l}{u:1411}{i:0}{s:"hi"}{i:0}{i:23}{i:0}{i:2}') 67 | ext.send_to_client('[0][0][0][6][5][131][0][0][0][0]') 68 | ext.send_to_client(HPacket.from_string('[0][0][0][6][5][131][0][0][0][0]', ext)) 69 | 70 | # string methods: 71 | packet = HPacket(1231, "hi", 5, "old", False, True, "lol") 72 | expression = packet.g_expression(ext) # G-Earth's predicted expression 73 | g_string = packet.g_string(ext) # G-Earth's string representation 74 | ``` 75 | Intercepting packets: 76 | ```python 77 | # intercept & print all packets 78 | def all_packets(message): 79 | packet = message.packet 80 | print(packet.g_string(ext)) 81 | 82 | ext.intercept(Direction.TO_CLIENT, all_packets) 83 | ext.intercept(Direction.TO_SERVER, all_packets) 84 | 85 | 86 | # intercept & parse specific packets 87 | def on_walk(message): 88 | (x, y) = message.packet.read('ii') 89 | print("Walking to x:{}, y={}".format(x, y)) 90 | 91 | def on_speech(message): 92 | (text, color, index) = message.packet.read('sii') 93 | message.is_blocked = (text == 'blocked') # block packet if speech equals "blocked" 94 | print("User said: {}".format(text)) 95 | 96 | ext.intercept(Direction.TO_SERVER, on_walk, 'RoomUserWalk') 97 | ext.intercept(Direction.TO_SERVER, on_speech, 'RoomUserTalk') 98 | ``` 99 | There is much more, such as: 100 | * packet manipulation 101 | * specific settings to be given to an Extension object 102 | * `hparsers`: example in `tests/user_profile.py` 103 | * `htools`: `tests/room_stuff.py` & `tests/inventory_items.py` 104 | -------------------------------------------------------------------------------- /g_python/hunitytools.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from .gextension import Extension 4 | from .hmessage import HMessage, Direction 5 | from .hpacket import HPacket 6 | from .hunityparsers import HUnityEntity, HFUnityFloorItem 7 | 8 | 9 | class UnityRoomUsers: 10 | def __init__(self, ext: Extension, users_in_room=28, get_guest_room=385, user_logged_out=29, status=34): 11 | self.room_users = {} 12 | self.__callback_new_users = None 13 | 14 | self.__ext = ext 15 | 16 | self.__lock = threading.Lock() 17 | 18 | ext.intercept(Direction.TO_CLIENT, self.__load_room_users, users_in_room) 19 | ext.intercept(Direction.TO_SERVER, self.__clear_room_users, get_guest_room) 20 | ext.intercept(Direction.TO_CLIENT, self.__remove_user, user_logged_out) 21 | ext.intercept(Direction.TO_CLIENT, self.__on_status, status) 22 | 23 | def __remove_user(self, message: HMessage): 24 | self.__start_remove_user_processing_thread(message.packet.read_int()) 25 | 26 | def __start_remove_user_processing_thread(self, index: int): 27 | thread = threading.Thread(target=self.__process_remove_user, args=(index,)) 28 | thread.start() 29 | 30 | def __process_remove_user(self, index: int): 31 | self.__lock.acquire() 32 | try: 33 | if index in self.room_users: 34 | del self.room_users[index] 35 | finally: 36 | self.__lock.release() 37 | 38 | def __load_room_users(self, message: HMessage): 39 | users = HUnityEntity.parse(message.packet) 40 | self.__start_user_processing_thread(users) 41 | 42 | if self.__callback_new_users is not None: 43 | self.__callback_new_users(users) 44 | 45 | def __process_users_in_room(self, entities): 46 | self.__lock.acquire() 47 | try: 48 | for user in entities: 49 | print(f'Adding entity {user}') 50 | self.room_users[user.index] = user 51 | finally: 52 | self.__lock.release() 53 | 54 | def __start_user_processing_thread(self, entities): 55 | thread = threading.Thread(target=self.__process_users_in_room, args=(entities,)) 56 | thread.start() 57 | 58 | def __clear_room_users(self, _): 59 | self.__lock.acquire() 60 | self.room_users.clear() 61 | self.__lock.release() 62 | 63 | def on_new_users(self, func): 64 | self.__callback_new_users = func 65 | 66 | def __on_status(self, message): 67 | thread = threading.Thread(target=self.__parse_and_apply_updates, args=(message.packet,)) 68 | thread.start() 69 | 70 | def __parse_and_apply_updates(self, packet): 71 | self.try_updates(HUnityStatus.parse(packet)) 72 | 73 | def __apply_updates(self, updates): 74 | for update in updates: 75 | self.__lock.acquire() 76 | try: 77 | user = self.room_users[update.index] 78 | if isinstance(user, HUnityEntity): 79 | user.try_update(update) 80 | except KeyError: 81 | pass 82 | finally: 83 | self.__lock.release() 84 | 85 | def __start_update_processing_thread(self, updates): 86 | thread = threading.Thread(target=self.__apply_updates, args=(updates,)) 87 | thread.start() 88 | 89 | def try_updates(self, updates): 90 | self.__start_update_processing_thread(updates) 91 | 92 | 93 | class UnityRoomFurni: 94 | def __init__(self, ext: Extension, floor_items=32, wall_items='RoomWallItems', 95 | request='RequestRoomHeightmap'): 96 | self.floor_furni = [] 97 | self.wall_furni = [] 98 | self.__callback_floor_furni = None 99 | self.__callback_wall_furni = None 100 | 101 | self.__ext = ext 102 | self.__request_id = request 103 | 104 | ext.intercept(Direction.TO_CLIENT, self.__floor_furni_load, floor_items) 105 | ext.intercept(Direction.TO_CLIENT, self.__wall_furni_load, wall_items) 106 | 107 | def __floor_furni_load(self, message): 108 | self.floor_furni = HFUnityFloorItem.parse(message.packet) 109 | if self.__callback_floor_furni is not None: 110 | self.__callback_floor_furni(self.floor_furni) 111 | 112 | def __wall_furni_load(self, message): 113 | self.wall_furni = HWallItem.parse(message.packet) 114 | if self.__callback_wall_furni is not None: 115 | self.__callback_wall_furni(self.wall_furni) 116 | 117 | def on_floor_furni_load(self, callback): 118 | self.__callback_floor_furni = callback 119 | 120 | def on_wall_furni_load(self, callback): 121 | self.__callback_wall_furni = callback 122 | 123 | def request(self): 124 | self.floor_furni = [] 125 | self.wall_furni = [] 126 | self.__ext.send_to_server(HPacket(self.__request_id)) 127 | -------------------------------------------------------------------------------- /g_python/hunityparsers.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from struct import unpack 3 | 4 | from .hparsers import HPoint, HEntityType, HDirection 5 | 6 | 7 | class HUnityEntity: 8 | def __init__(self, packet): 9 | self.id, self.name, self.motto, self.figure_id, self.index, x, y, z, _, entity_type_id = \ 10 | packet.read('lsssiiisii') 11 | self.tile = get_tile_from_coords(x, y, z) 12 | self.nextTile = None 13 | self.headFacing = None 14 | self.bodyFacing = None 15 | self.entity_type = HEntityType(entity_type_id) 16 | 17 | self.stuff = [] 18 | if self.entity_type == HEntityType.HABBO: 19 | self.gender = packet.read_string() 20 | self.stuff.extend(packet.read('iii')) 21 | self.favorite_group = packet.read_string() 22 | self.stuff.extend(packet.read('siB')) 23 | elif self.entity_type == HEntityType.PET: 24 | self.stuff.append(packet.read_int()) 25 | self.owner_id, self.owner_name = packet.read('ls') 26 | self.stuff.extend(packet.read("iisis")) 27 | elif self.entity_type == HEntityType.BOT: 28 | self.gender, self.owner_id, self.owner_name, arr_length = packet.read('slsu') 29 | self.stuff.append(arr_length) 30 | for _ in range(arr_length): 31 | self.stuff.append(packet.read_short()) 32 | 33 | def __str__(self): 34 | return ' [{}] {} - {}'.format(self.index, self.name, self.entity_type.name) 35 | 36 | def try_update(self, update): 37 | self.tile = update.tile 38 | self.nextTile = update.nextTile 39 | self.headFacing = update.headFacing 40 | self.bodyFacing = update.bodyFacing 41 | 42 | 43 | @classmethod 44 | def parse(cls, packet): 45 | return [HUnityEntity(packet) for _ in range(packet.read_short())] 46 | 47 | 48 | 49 | 50 | class HUnityStatus: 51 | def __init__(self, packet): 52 | self.index, x, y, z, head, body, self.action = packet.read('iiisiis') 53 | self.tile = get_tile_from_coords(x, y, z) 54 | self.headFacing = HDirection(head) 55 | self.bodyFacing = HDirection(body) 56 | self.nextTile = self.predict_next_tile() 57 | 58 | def __str__(self): 59 | return ' [{}] - X: {} - Y: {} - Z: {} - head {} - body {} - next tile {}'\ 60 | .format(self.index, self.tile.x, self.tile.y, self.tile.z, self.headFacing.name, self.bodyFacing.name, self.nextTile) 61 | 62 | def predict_next_tile(self): 63 | actions = self.action.split('/mv ') 64 | if len(actions) > 1: 65 | (x, y, z) = actions[1].replace('/', '').split(',') 66 | return get_tile_from_coords(int(x), int(y), z) 67 | else: 68 | return HPoint(-1, -1, 0.0) 69 | 70 | @classmethod 71 | def parse(cls, packet): 72 | return [HUnityStatus(packet) for _ in range(packet.read_short())] 73 | 74 | 75 | def get_tile_from_coords(x, y, z) -> HPoint: 76 | try: 77 | z = float(z) 78 | except ValueError: 79 | z = 0.0 80 | 81 | return HPoint(x, y, z) 82 | 83 | 84 | def read_stuff(packet, category): 85 | stuff = [] 86 | cat2 = category & 0xFF 87 | 88 | if cat2 == 0: # legacy 89 | stuff.append(packet.read_string()) 90 | if cat2 == 1: # map 91 | stuff.append([packet.read('ss') for _ in range(packet.read_short())]) 92 | if cat2 == 2: # string array 93 | stuff.append([packet.read_string() for _ in range(packet.read_short())]) 94 | if cat2 == 3: # vote results 95 | stuff.extend(packet.read('si')) 96 | if cat2 == 5: # int array 97 | stuff.append([packet.read_int() for _ in range(packet.read_short())]) 98 | if cat2 == 6: # highscores 99 | stuff.extend(packet.read('sii')) 100 | stuff.append([(packet.read_int(), [packet.read_string() for _ in range(packet.read_short())]) for _ in 101 | range(packet.read_int())]) 102 | if cat2 == 7: # crackables 103 | stuff.extend(packet.read('sii')) 104 | 105 | if (category & 0xFF00 & 0x100) > 0: 106 | stuff.extend(packet.read('ii')) 107 | 108 | return stuff 109 | 110 | 111 | class HFUnityFloorItem: 112 | def __init__(self, packet): 113 | self.id, self.type_id, x, y, facing_id = packet.read('liiii') 114 | 115 | # https://en.wikipedia.org/wiki/IEEE_754 116 | z = unpack('>f', bytearray(packet.read('bbbb')))[0] 117 | 118 | self.tile = HPoint(x, y, z) 119 | self.facing = HDirection(facing_id) 120 | 121 | # another weird float 122 | self.height = unpack('>f', bytearray(packet.read('bbbb')))[0] 123 | 124 | a, b, self.category = packet.read('iii') 125 | self.stuff = read_stuff(packet, self.category) 126 | 127 | self.seconds_to_expiration, self.usage_policy, self.owner_id = packet.read('iil') 128 | self.owner = None # expected to be filled in by parse class method 129 | 130 | if self.type_id < 0: 131 | packet.read_string() 132 | 133 | @classmethod 134 | def parse(cls, packet): 135 | owners = {} 136 | for _ in range(packet.read_short()): 137 | id = packet.read_long() 138 | owners[id] = packet.read_string() 139 | 140 | furnis = [HFUnityFloorItem(packet) for _ in range(packet.read_short())] 141 | for furni in furnis: 142 | furni.owner = owners[furni.owner_id] 143 | 144 | return furnis -------------------------------------------------------------------------------- /g_python/gbot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from datetime import datetime 4 | from typing import Callable, TypedDict, NotRequired 5 | 6 | from g_python.gextension import Extension, InterceptMethod 7 | from g_python.hdirection import Direction 8 | from g_python.hmessage import HMessage 9 | from g_python.hpacket import HPacket 10 | 11 | 12 | class HBotProfile(TypedDict): 13 | id: NotRequired[int] 14 | username: NotRequired[str] 15 | gender: NotRequired[int] 16 | is_following_allowed: NotRequired[bool] 17 | figure: NotRequired[str] 18 | motto: NotRequired[str] 19 | category_id: NotRequired[int] 20 | creation_date: NotRequired[str] 21 | achievement_score: NotRequired[int] 22 | friend_count: NotRequired[int] 23 | is_friend: NotRequired[bool] 24 | is_requested_friend: NotRequired[bool] 25 | is_online: NotRequired[bool] 26 | is_persisted_message_user: NotRequired[bool] 27 | is_vip_member: NotRequired[bool] 28 | is_pocket_habbo_user: NotRequired[bool] 29 | 30 | # def __init__(self, **kwargs) -> None: 31 | # for key, value in HBotProfile.settings.items(): 32 | # setattr(self, key, value if key not in kwargs else kwargs[key]) 33 | 34 | 35 | DEFAULT_PROFILE: HBotProfile = { 36 | "id": -1, 37 | "username": "Bot", 38 | "gender": 1, 39 | "is_following_allowed": False, 40 | "figure": "hd-3704-29", 41 | "motto": "Hey! I'm a bot.", 42 | "category_id": 0, 43 | "creation_date": datetime.today().strftime("%m-%d-%Y"), 44 | "achievement_score": 1000, 45 | "friend_count": 1, 46 | "is_friend": True, 47 | "is_requested_friend": False, 48 | "is_online": True, 49 | "is_persisted_message_user": True, 50 | "is_vip_member": False, 51 | "is_pocket_habbo_user": False, 52 | } 53 | 54 | 55 | def fill_profile(profile: HBotProfile) -> None: 56 | profile.setdefault("id", random.randrange(1 << 30, 1 << 31)) 57 | for key in DEFAULT_PROFILE: 58 | if key != "id": 59 | profile.setdefault(key, DEFAULT_PROFILE[key]) 60 | 61 | 62 | # Thanks to denio4321 and sirjonasxx I got some ideas from them 63 | class ConsoleBot: 64 | def __init__( 65 | self, extension: Extension, prefix: str = ":", bot_settings: HBotProfile | None = None 66 | ) -> None: 67 | self._extension = extension 68 | self._prefix = prefix 69 | self._bot_settings = {} if bot_settings is None else bot_settings 70 | fill_profile(self._bot_settings) 71 | self._commands = {} 72 | 73 | self._chat_opened = False 74 | self._once_per_connection = False 75 | 76 | extension.intercept(Direction.TO_SERVER, self.should_open_chat) 77 | extension.intercept( 78 | Direction.TO_CLIENT, self.on_friend_list, "FriendListFragment", mode=InterceptMethod.ASYNC 79 | ) 80 | extension.intercept(Direction.TO_SERVER, self.on_send_message, "SendMsg") 81 | extension.intercept( 82 | Direction.TO_SERVER, self.on_get_profile, "GetExtendedProfile" 83 | ) 84 | 85 | def should_open_chat(self, hmessage: HMessage) -> None: 86 | if not self._chat_opened: 87 | self._chat_opened = True 88 | 89 | if hmessage.packet.header_id != 4000: 90 | self._once_per_connection = True 91 | self.create_chat() 92 | 93 | def on_friend_list(self, _ignored_hmessage: HMessage) -> None: 94 | if not self._once_per_connection: 95 | self._once_per_connection = True 96 | 97 | time.sleep(1) 98 | self.create_chat() 99 | 100 | def on_send_message(self, hmessage: HMessage) -> None: 101 | packet = hmessage.packet 102 | 103 | if packet.read_int() == self._bot_settings.id: 104 | hmessage.is_blocked = True 105 | message = packet.read_string() 106 | 107 | prefix, raw_message = message[0], message[1:] 108 | 109 | if message.startswith(prefix) and raw_message in self._commands.keys(): 110 | self._commands[raw_message]() 111 | 112 | def on_get_profile(self, hmessage: HMessage) -> None: 113 | if hmessage.packet.read_int() == self._bot_settings["id"]: 114 | bot = self._bot_settings 115 | 116 | packet = HPacket("ExtendedProfile", bot["id"], bot["username"], bot["figure"], bot["motto"], 117 | bot["creation_date"], bot["achievement_score"], bot["friend_count"], bot["is_friend"], 118 | bot["is_requested_friend"], bot["is_online"], 0, -255, True) 119 | 120 | self._extension.send_to_client(packet) 121 | 122 | self._extension.send_to_client( 123 | HPacket("HabboUserBadges", bot["id"], 1, 1, "BOT") 124 | ) 125 | 126 | def create_chat(self) -> None: 127 | bot = self._bot_settings 128 | 129 | packet = HPacket("FriendListUpdate", 0, 1, False, False, "", bot["id"], bot["username"], bot["gender"], 130 | bot["is_online"], bot["is_following_allowed"], bot["figure"], bot["category_id"], 131 | bot["motto"], 0, bot["is_persisted_message_user"], bot["is_vip_member"], 132 | bot["is_pocket_habbo_user"], 65537) 133 | 134 | self._extension.send_to_client(packet) 135 | 136 | def send(self, message: str, as_invite: bool = False) -> None: 137 | if as_invite: 138 | self._extension.send_to_client( 139 | HPacket("RoomInvite", self._bot_settings.id, message) 140 | ) 141 | 142 | return None 143 | 144 | self._extension.send_to_client( 145 | HPacket("NewConsole", self._bot_settings.id, message, 0, "") 146 | ) 147 | 148 | def add_command(self, command: str, callback: Callable[[], None]) -> None: 149 | self._commands[command] = callback 150 | -------------------------------------------------------------------------------- /g_python/htools.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from .gextension import Extension, ConsoleColour 4 | from .hmessage import HMessage, Direction 5 | from .hpacket import HPacket 6 | from .hparsers import HEntity, HFloorItem, HWallItem, HInventoryItem, HUserUpdate 7 | import sys 8 | 9 | 10 | def validate_headers(ext: Extension, parser_name: str, headers: list[tuple[int | str, Direction]]): 11 | def validate(): 12 | for (header, direction) in headers: 13 | if header is None: 14 | error = "Missing headerID/Name in '{}'".format(parser_name) 15 | print(error, file=sys.stderr) 16 | ext.write_to_console(error, ConsoleColour.RED) 17 | if isinstance(header, str) and (ext.packet_infos is None or header not in ext.packet_infos[direction]): 18 | error = "Invalid headerID/Name in '{}': {}".format(parser_name, header) 19 | print(error, file=sys.stderr) 20 | ext.write_to_console(error, ConsoleColour.RED) 21 | 22 | ext.on_event('connection_start', validate) 23 | if ext.connection_info is not None: 24 | validate() 25 | 26 | 27 | class RoomUsers: 28 | def __init__(self, ext: Extension, room_users: str | int = 'Users', room_model: str | int = 'RoomReady', 29 | remove_user: str | int = 'UserRemove', 30 | request: str | int = 'GetHeightMap', status: str | int = 'UserUpdate'): 31 | validate_headers(ext, 'RoomUsers', [ 32 | (room_users, Direction.TO_CLIENT), 33 | (room_model, Direction.TO_CLIENT), 34 | (remove_user, Direction.TO_CLIENT), 35 | (request, Direction.TO_SERVER)]) 36 | 37 | self.room_users = {} 38 | self.__callback_new_users = None 39 | self.__callback_remove_user = None 40 | 41 | self.__ext = ext 42 | self.__request_id = request 43 | 44 | ext.intercept(Direction.TO_CLIENT, self.__load_room_users, room_users) 45 | ext.intercept(Direction.TO_CLIENT, self.__clear_room_users, room_model) # (clear users / new room entered) 46 | ext.intercept(Direction.TO_CLIENT, self.__remove_user, remove_user) 47 | ext.intercept(Direction.TO_CLIENT, self.__on_status, status) 48 | 49 | def __remove_user(self, message: HMessage) -> None: 50 | index = int(message.packet.read_string()) 51 | if index in self.room_users: 52 | user = self.room_users[index] 53 | del self.room_users[index] 54 | if self.__callback_remove_user is not None: 55 | self.__callback_remove_user(user) 56 | 57 | def __load_room_users(self, message: HMessage) -> None: 58 | users = HEntity.parse(message.packet) 59 | for user in users: 60 | self.room_users[user.index] = user 61 | 62 | if self.__callback_new_users is not None: 63 | self.__callback_new_users(users) 64 | 65 | def __clear_room_users(self, _) -> None: 66 | self.room_users.clear() 67 | 68 | def on_new_users(self, func: Callable[[list[HEntity]], None]) -> None: 69 | self.__callback_new_users = func 70 | 71 | def on_remove_user(self, func: Callable[[HEntity], None]) -> None: 72 | self.__callback_remove_user = func 73 | 74 | def __on_status(self, message: HMessage) -> None: 75 | self.try_updates(HUserUpdate.parse(message.packet)) 76 | 77 | def try_updates(self, updates: list[HUserUpdate]) -> None: 78 | for update in updates: 79 | try: 80 | user = self.room_users[update.index] 81 | if isinstance(user, HEntity): 82 | user.try_update(update) 83 | except KeyError: 84 | pass 85 | 86 | def request(self) -> None: 87 | self.room_users = {} 88 | self.__ext.send_to_server(HPacket(self.__request_id)) 89 | 90 | 91 | class RoomFurni: 92 | def __init__(self, ext: Extension, floor_items: str | int = 'Objects', wall_items: str | int = 'Items', 93 | request: str | int = 'GetHeightMap'): 94 | validate_headers(ext, 'RoomFurni', [ 95 | (floor_items, Direction.TO_CLIENT), 96 | (wall_items, Direction.TO_CLIENT), 97 | (request, Direction.TO_SERVER)]) 98 | 99 | self.floor_furni = [] 100 | self.wall_furni = [] 101 | self.__callback_floor_furni = None 102 | self.__callback_wall_furni = None 103 | 104 | self.__ext = ext 105 | self.__request_id = request 106 | 107 | ext.intercept(Direction.TO_CLIENT, self.__floor_furni_load, floor_items) 108 | ext.intercept(Direction.TO_CLIENT, self.__wall_furni_load, wall_items) 109 | 110 | def __floor_furni_load(self, message: HMessage) -> None: 111 | self.floor_furni = HFloorItem.parse(message.packet) 112 | if self.__callback_floor_furni is not None: 113 | self.__callback_floor_furni(self.floor_furni) 114 | 115 | def __wall_furni_load(self, message: HMessage) -> None: 116 | self.wall_furni = HWallItem.parse(message.packet) 117 | if self.__callback_wall_furni is not None: 118 | self.__callback_wall_furni(self.wall_furni) 119 | 120 | def on_floor_furni_load(self, callback: Callable[[list[HFloorItem]], None]) -> None: 121 | self.__callback_floor_furni = callback 122 | 123 | def on_wall_furni_load(self, callback: Callable[[list[HWallItem]], None]) -> None: 124 | self.__callback_wall_furni = callback 125 | 126 | def request(self) -> None: 127 | self.floor_furni = [] 128 | self.wall_furni = [] 129 | self.__ext.send_to_server(HPacket(self.__request_id)) 130 | 131 | 132 | class Inventory: 133 | def __init__(self, ext: Extension, inventory_items: str | int = 'FurniList', 134 | request: str | int = 'RequestFurniInventory'): 135 | validate_headers(ext, 'Inventory', [ 136 | (inventory_items, Direction.TO_CLIENT), 137 | (request, Direction.TO_SERVER)]) 138 | 139 | self.loaded = False 140 | self.is_loading = False 141 | self.inventory_items = [] 142 | self.__inventory_items_buffer = [] 143 | 144 | self.__ext = ext 145 | self.__request_id = request 146 | self.__inventory_load_callback = None 147 | 148 | ext.intercept(Direction.TO_CLIENT, self.__user_inventory_load, inventory_items) 149 | 150 | def __user_inventory_load(self, message: HMessage) -> None: 151 | packet = message.packet 152 | total, current = packet.read('ii') 153 | packet.reset() 154 | 155 | items = HInventoryItem.parse(packet) 156 | 157 | if current == 0: # fresh inventory load 158 | self.__inventory_items_buffer.clear() 159 | self.is_loading = True 160 | 161 | self.__inventory_items_buffer.extend(items) 162 | # print("Loading inventory.. ({}/{})".format(current + 1, total)) 163 | 164 | if current == total - 1: # latest packet 165 | self.is_loading = False 166 | self.loaded = True 167 | self.inventory_items = list(self.__inventory_items_buffer) 168 | self.__inventory_items_buffer.clear() 169 | 170 | self.__inventory_load_callback(self.inventory_items) 171 | 172 | def request(self) -> None: 173 | self.__ext.send_to_server(HPacket(self.__request_id)) 174 | 175 | def on_inventory_load(self, callback: Callable[[list[HInventoryItem]], None]) -> None: 176 | self.__inventory_load_callback = callback 177 | -------------------------------------------------------------------------------- /g_python/hpacket.py: -------------------------------------------------------------------------------- 1 | from typing import Self 2 | 3 | from .gextension import Extension 4 | from .hdirection import Direction 5 | 6 | 7 | class HPacket: 8 | default_extension: Extension | None = None 9 | 10 | def __init__(self, identifier: int | str, *objects: str | int | bool | bytes): 11 | self.incomplete_identifier = None if (type(identifier) is int) else identifier 12 | 13 | self.read_index = 6 14 | self.bytearray = bytearray(b'\x00\x00\x00\x02\xff\xff') 15 | if self.incomplete_identifier is None: 16 | self.replace_short(4, identifier) 17 | self.is_edited = False 18 | 19 | for obj in objects: 20 | if type(obj) is str: 21 | self.append_string(obj) 22 | if type(obj) is int: 23 | self.append_int(obj) 24 | if type(obj) is bool: 25 | self.append_bool(obj) 26 | if type(obj) is bytes: 27 | self.append_bytes(obj) 28 | 29 | self.is_edited = False 30 | 31 | def fill_id(self, direction: Direction, extension: Extension | None = None) -> bool: 32 | if self.incomplete_identifier is not None: 33 | if extension is None: 34 | if self.default_extension is None: 35 | return False 36 | extension = self.default_extension 37 | 38 | if extension.packet_infos is not None and self.incomplete_identifier in extension.packet_infos[direction]: 39 | edited_old = self.is_edited 40 | self.replace_short(4, extension.packet_infos[direction][self.incomplete_identifier][0]['Id']) 41 | self.is_edited = edited_old 42 | self.incomplete_identifier = None 43 | return True 44 | return False 45 | return True 46 | 47 | # https://stackoverflow.com/questions/682504/what-is-a-clean-pythonic-way-to-have-multiple-constructors-in-python 48 | @classmethod 49 | def from_bytes(cls, byte_list: bytes) -> Self: 50 | obj = cls.__new__(cls) # Does not call __init__ 51 | super(HPacket, obj).__init__() # Don't forget to call any polymorphic base class initializers 52 | obj.bytearray = bytearray(byte_list) 53 | obj.read_index = 6 54 | obj.is_edited = False 55 | return obj 56 | 57 | @classmethod 58 | def from_string(cls, string: str, extension: Extension | None = None) -> Self: 59 | if extension is None: 60 | if HPacket.default_extension is None: 61 | raise Exception('No extension given for string <-> packet conversion') 62 | else: 63 | extension = HPacket.default_extension 64 | return extension.string_to_packet(string) 65 | 66 | @classmethod 67 | def reconstruct_from_java(cls, string: str) -> Self: 68 | obj = cls.__new__(cls) 69 | super(HPacket, obj).__init__() 70 | obj.read_index = 6 71 | 72 | obj.bytearray = bytearray(string[1:].encode("iso-8859-1")) 73 | obj.is_edited = string[0] == '1' 74 | obj.incomplete_identifier = None 75 | return obj 76 | 77 | def __repr__(self) -> str: 78 | return ('1' if self.is_edited else '0') + self.bytearray.decode("iso-8859-1") 79 | 80 | def __bytes__(self) -> bytes: 81 | return bytes(self.bytearray) 82 | 83 | def __len__(self) -> int: 84 | return self.read_int(0) 85 | 86 | def __str__(self) -> str: 87 | return "(id:{}, length:{}) -> {}".format( 88 | self.header_id() if not self.is_incomplete_packet() else self.incomplete_identifier, 89 | len(self), 90 | bytes(self) 91 | ) 92 | 93 | def is_incomplete_packet(self) -> bool: 94 | return self.incomplete_identifier is not None 95 | 96 | def g_string(self, extension: Extension | None = None) -> str: 97 | if extension is None: 98 | if HPacket.default_extension is None: 99 | raise Exception('No extension given for packet <-> string conversion') 100 | else: 101 | extension = HPacket.default_extension 102 | 103 | return extension.packet_to_string(self) 104 | 105 | def g_expression(self, extension: Extension | None = None) -> str: 106 | if extension is None: 107 | if HPacket.default_extension is None: 108 | raise Exception('No extension given for packet <-> string conversion') 109 | else: 110 | extension = HPacket.default_extension 111 | 112 | return extension.packet_to_expression(self) 113 | 114 | def is_corrupted(self) -> bool: 115 | return len(self.bytearray) < 6 or self.read_int(0) != len(self.bytearray) - 4 116 | 117 | def reset(self) -> None: 118 | self.read_index = 6 119 | 120 | def header_id(self) -> int: 121 | return self.read_short(4) 122 | 123 | def fix_length(self) -> None: 124 | self.replace_int(0, len(self.bytearray) - 4) 125 | 126 | def read_int(self, index=None) -> int: 127 | if index is None: 128 | index = self.read_index 129 | self.read_index += 4 130 | 131 | return int.from_bytes(self.bytearray[index:index + 4], byteorder='big', signed=True) 132 | 133 | def read_short(self, index=None) -> int: 134 | if index is None: 135 | index = self.read_index 136 | self.read_index += 2 137 | 138 | return int.from_bytes(self.bytearray[index:index + 2], byteorder='big', signed=True) 139 | 140 | def read_long(self, index=None) -> int: 141 | if index is None: 142 | index = self.read_index 143 | self.read_index += 8 144 | 145 | return int.from_bytes(self.bytearray[index:index + 8], byteorder='big', signed=True) 146 | 147 | def read_string(self, index=None, head: int = 2, encoding: str = 'iso-8859-1') -> str: 148 | if index is None: 149 | index = self.read_index 150 | self.read_index += head + int.from_bytes(self.bytearray[index:index + head], byteorder='big', signed=False) 151 | 152 | length = int.from_bytes(self.bytearray[index:index + head], byteorder='big', signed=False) 153 | return self.bytearray[index + head:index + head + length].decode(encoding) 154 | 155 | def read_bytes(self, length: int, index: int | None = None) -> bytearray: 156 | if index is None: 157 | index = self.read_index 158 | self.read_index += length 159 | 160 | return self.bytearray[index:index + length] 161 | 162 | def read_byte(self, index: int | None = None) -> int: 163 | if index is None: 164 | index = self.read_index 165 | self.read_index += 1 166 | 167 | return self.bytearray[index] 168 | 169 | def read_bool(self, index: int | None = None) -> bool: 170 | return self.read_byte(index) != 0 171 | 172 | def read(self, structure: str) -> list: 173 | read_methods = { 174 | 'i': self.read_int, 175 | 's': self.read_string, 176 | 'b': self.read_byte, 177 | 'B': self.read_bool, 178 | 'u': self.read_short, 179 | 'l': self.read_long 180 | } 181 | return [read_methods[value_type]() for value_type in structure] 182 | 183 | def replace_int(self, index: int, value: int) -> None: 184 | self.bytearray[index:index + 4] = value.to_bytes(4, byteorder='big', signed=True) 185 | self.is_edited = True 186 | 187 | def replace_short(self, index: int, value: int) -> None: 188 | self.bytearray[index:index + 2] = value.to_bytes(2, byteorder='big', signed=True) 189 | self.is_edited = True 190 | 191 | def replace_long(self, index: int, value: int) -> None: 192 | self.bytearray[index:index + 8] = value.to_bytes(8, byteorder='big', signed=False) 193 | self.is_edited = True 194 | 195 | def replace_bool(self, index: int, value: bool) -> None: 196 | self.bytearray[index] = value 197 | self.is_edited = True 198 | 199 | def replace_string(self, index: int, value: str, encoding: str = 'utf-8') -> None: 200 | old_len = self.read_short(index) 201 | part1 = self.bytearray[0:index] 202 | part3 = self.bytearray[index + 2 + old_len:] 203 | 204 | new_string = value.encode(encoding) 205 | new_len = len(new_string) 206 | part2 = new_len.to_bytes(2, byteorder='big', signed=False) + new_string 207 | 208 | self.bytearray = part1 + part2 + part3 209 | self.fix_length() 210 | self.is_edited = True 211 | 212 | def append_int(self, value: int) -> Self: 213 | self.bytearray.extend(value.to_bytes(4, byteorder='big', signed=True)) 214 | self.fix_length() 215 | self.is_edited = True 216 | return self 217 | 218 | def append_short(self, value: int) -> Self: 219 | self.bytearray.extend(value.to_bytes(2, byteorder='big', signed=True)) 220 | self.fix_length() 221 | self.is_edited = True 222 | return self 223 | 224 | def append_long(self, value: int) -> Self: 225 | self.bytearray.extend(value.to_bytes(8, byteorder='big', signed=False)) 226 | self.fix_length() 227 | self.is_edited = True 228 | return self 229 | 230 | def append_bytes(self, value: bytes) -> Self: 231 | self.bytearray.extend(value) 232 | self.fix_length() 233 | self.is_edited = True 234 | return self 235 | 236 | def append_bool(self, value: bool) -> Self: 237 | self.append_bytes(b'\x01' if value else b'\x00') 238 | self.fix_length() 239 | self.is_edited = True 240 | return self 241 | 242 | def append_string(self, value: str, head: int = 2, encoding: str = 'utf-8'): 243 | b = value.encode(encoding) 244 | self.bytearray.extend(len(b).to_bytes(head, byteorder='big', signed=False) + b) 245 | self.fix_length() 246 | self.is_edited = True 247 | return self 248 | -------------------------------------------------------------------------------- /g_python/hparsers.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, StrEnum 2 | from typing import Self, TypedDict 3 | 4 | from g_python.hpacket import HPacket 5 | 6 | 7 | class HClientHost(StrEnum): 8 | BRAZIL = "game-br.habbo.com" 9 | GERMANY = "game-de.habbo.com" 10 | SPAIN = "game-es.habbo.com" 11 | FINLAND = "game-fi.habbo.com" 12 | FRANCE = "game-fr.habbo.com" 13 | ITALY = "game-it.habbo.com" 14 | NETHERLANDS = "game-nl.habbo.com" 15 | TURKEY = "game-tr.habbo.com" 16 | UNITED_STATES = "game-us.habbo.com" 17 | SANDBOX = "game-s2.habbo.com" 18 | 19 | 20 | class HGroupMode(IntEnum): 21 | OPEN = 0 22 | ADMINAPPROVAL = 1 23 | CLOSED = 2 24 | 25 | 26 | class HBubble(IntEnum): 27 | NORMAL = 0 28 | RED = 3 29 | BLUE = 4 30 | YELLOW = 5 31 | GREEN = 6 32 | BLACK = 7 33 | ZOMBIE = 9 34 | SKULL = 10 35 | PINK = 12 36 | PURPLE = 13 37 | ORANGE = 14 38 | HEART = 16 39 | ROSE = 17 40 | PIG = 19 41 | DOG = 20 42 | DUCK = 21 43 | DRAGON = 22 44 | STAFF = 23 45 | BATS = 24 46 | CONSOLE = 25 47 | STORM = 27 48 | PIRATE = 29 49 | AMBASSADOR = 37 50 | 51 | 52 | class HDance(IntEnum): 53 | NONE = 0 54 | NORMAL = 1 55 | POGOMOGO = 2 56 | DUCKFUNK = 3 57 | THEROLLIE = 4 58 | 59 | 60 | class HAction(IntEnum): 61 | NONE = 0 62 | MOVE = 1 63 | SIT = 2 64 | LAY = 3 65 | SIGN = 4 66 | 67 | 68 | class HDirection(IntEnum): 69 | NORTH = 0 70 | NORTHEAST = 1 71 | EAST = 2 72 | SOUTHEAST = 3 73 | SOUTH = 4 74 | SOUTHWEST = 5 75 | WEST = 6 76 | NORTHWEST = 7 77 | 78 | 79 | class HEntityType(IntEnum): 80 | HABBO = 1 81 | PET = 2 82 | OLD_BOT = 3 83 | BOT = 4 84 | 85 | 86 | class HGender(StrEnum): 87 | UNISEX = "U" 88 | MALE = "M" 89 | FEMALE = "F" 90 | 91 | 92 | class HSign(IntEnum): 93 | ZERO = 0 94 | ONE = 1 95 | TWO = 2 96 | THREE = 3 97 | FOUR = 4 98 | FIVE = 5 99 | SIX = 6 100 | SEVEN = 7 101 | EIGHT = 8 102 | NINE = 9 103 | TEN = 10 104 | HEART = 11 105 | SKULL = 12 106 | EXCLAMATION = 13 107 | SOCCERBALL = 14 108 | SMILE = 15 109 | REDCARD = 16 110 | YELLOWCARD = 17 111 | INVISIBLE = 18 112 | 113 | 114 | class HStance(IntEnum): 115 | STAND = 0 116 | SIT = 1 117 | LAY = 2 118 | 119 | 120 | class HRelationshipStatus(IntEnum): 121 | NONE = 0 122 | HEART = 1 123 | SMILEY = 2 124 | SKULL = 3 125 | 126 | 127 | class HSpecialType(IntEnum): 128 | DEFAULT = 1 129 | WALLPAPER = 2 130 | FLOORPAINT = 3 131 | LANDSCAPE = 4 132 | POSTIT = 5 133 | POSTER = 6 134 | SOUNDSET = 7 135 | TRAXSONG = 8 136 | PRESENT = 9 137 | ECOTRONBOX = 10 138 | TROPHY = 11 139 | CREDITFURNI = 12 140 | PETSHAMPOO = 13 141 | PETCUSTOMPART = 14 142 | PETCUSTOMPARTSHAMPOO = 15 143 | PETSADDLE = 16 144 | GUILDFURNI = 17 145 | GAMEFURNI = 18 146 | MONSTERPLANTSEED = 19 147 | MONSTERPLANTREVIVIAL = 20 148 | MONSTERPLANTREBREED = 21 149 | MONSTERPLANTFERTILIZE = 22 150 | FIGUREPURCHASABLESET = 23 151 | 152 | 153 | class HDoorMode(IntEnum): 154 | OPEN = 0 155 | DOORBELL = 1 156 | PASSWORD = 2 157 | INVISIBLE = 3 158 | 159 | 160 | class HProductType(StrEnum): 161 | WALLITEM = 'I' 162 | FLOORITEM = 'S' 163 | EFFECT = 'E' 164 | BADGE = 'B' 165 | 166 | 167 | class HPoint: 168 | def __init__(self, x: int, y: int, z: float = 0.0): 169 | self.x = x 170 | self.y = y 171 | self.z = z 172 | 173 | def __str__(self) -> str: 174 | return "x: {}, y: {}, z: {}".format(self.x, self.y, self.z) 175 | 176 | def __repr__(self) -> str: 177 | return "HPoint({},{},{})".format(self.x, self.y, self.z) 178 | 179 | 180 | class HUserUpdate: 181 | def __init__(self, packet: HPacket): 182 | self.index, x, y, z, head, body, self.action = packet.read('iiisiis') 183 | self.tile = get_tile_from_coords(x, y, z) 184 | self.headFacing = HDirection(head) 185 | self.bodyFacing = HDirection(body) 186 | self.nextTile = self.predict_next_tile() 187 | 188 | def __str__(self): 189 | return ' [{}] - X: {} - Y: {} - Z: {} - head {} - body {} - next tile {}' \ 190 | .format(self.index, self.tile.x, self.tile.y, self.tile.z, self.headFacing.name, self.bodyFacing.name, 191 | self.nextTile) 192 | 193 | def predict_next_tile(self): 194 | actions = self.action.split('/mv ') 195 | if len(actions) > 1: 196 | (x, y, z) = actions[1].replace('/', '').split(',') 197 | return get_tile_from_coords(int(x), int(y), z) 198 | else: 199 | return HPoint(-1, -1, 0.0) 200 | 201 | @classmethod 202 | def parse(cls, packet): 203 | return [HUserUpdate(packet) for _ in range(packet.read_int())] 204 | 205 | 206 | class HEntity: 207 | def __init__(self, packet: HPacket): 208 | self.id, self.name, self.motto, self.figure_id, self.index, x, y, z, facing_id, entity_type_id = \ 209 | packet.read('isssiiisii') 210 | self.tile = HPoint(x, y, float(z)) 211 | self.nextTile = None 212 | self.headFacing = HDirection(facing_id) 213 | self.bodyFacing = HDirection(facing_id) 214 | self.entity_type = HEntityType(entity_type_id) 215 | 216 | self.stuff = [] 217 | if self.entity_type == HEntityType.HABBO: 218 | self.gender = packet.read_string() 219 | self.stuff.extend(packet.read('ii')) 220 | self.favorite_group = packet.read_string() 221 | self.stuff.extend(packet.read('siB')) 222 | elif self.entity_type == HEntityType.PET: 223 | self.stuff.extend(packet.read('iisiBBBBBBis')) 224 | elif self.entity_type == HEntityType.BOT: 225 | self.stuff.extend(packet.read('sis')) 226 | self.stuff.append([packet.read_short() for _ in range(packet.read_int())]) 227 | 228 | def __str__(self) -> str: 229 | return '{}: {} - {}'.format(self.index, self.name, self.entity_type.name) 230 | 231 | def try_update(self, update: HUserUpdate) -> None: 232 | if self.index == update.index: 233 | self.tile = update.tile 234 | self.nextTile = update.nextTile 235 | self.headFacing = update.headFacing 236 | self.bodyFacing = update.bodyFacing 237 | 238 | @classmethod 239 | def parse(cls, packet: HPacket) -> list[Self]: 240 | return [HEntity(packet) for _ in range(packet.read_int())] 241 | 242 | 243 | """ 244 | class HFriends: 245 | def __init__(self, packet): 246 | self.friends = [] 247 | _, _ = packet.read('ii') 248 | self.total_friends = packet.read_int() 249 | for _ in range(self.total_friends): 250 | id_user, name, _, _, _, clothes, _, motto = packet.read('isiBBsis') 251 | if packet.read_int() == 0: 252 | _, _ = packet.read('BB') 253 | _, _ = packet.read('Bs') 254 | self.friends.append([id_user, name, clothes, motto]) 255 | """ 256 | 257 | 258 | class HFriend: 259 | def __init__(self, packet: HPacket): 260 | self.id, self.name, gender_id, self.online, self.following_allowed, self.figure, self.category_id, \ 261 | self.motto, self.real_name, self.facebook_id, self.persisted_message_user, self.vip_member, \ 262 | self.pocket_habbo_user, relationship_status_id = packet.read('isiBBsisssBBBu') 263 | 264 | self.gender = HGender.FEMALE if gender_id == 0 else HGender.MALE 265 | self.relationship_status = HRelationshipStatus(relationship_status_id) 266 | 267 | def __str__(self) -> str: 268 | return "id: {}, name: {}, gender: {}, relationship status: {}" \ 269 | .format(self.id, self.name, self.gender, self.relationship_status.name) 270 | 271 | @classmethod 272 | def parse_from_fragment(cls, packet: HPacket) -> list[Self]: 273 | # int packetCount skipped 274 | # int packetIndex skipped 275 | packet.read_index = 14 276 | return [HFriend(packet) for _ in range(packet.read_int())] 277 | 278 | @classmethod 279 | def parse_from_update(cls, packet: HPacket) -> list[Self]: 280 | categories = {} 281 | for _ in range(packet.read_int()): 282 | cat_id = packet.read_int() 283 | categories[cat_id] = packet.read_string() 284 | 285 | friends = [HFriend(packet) for _ in range(packet.read_int())] 286 | 287 | for friend in friends: 288 | friend.category_name = categories.get(friend.category_id, None) 289 | 290 | return friends 291 | 292 | 293 | def read_stuff(packet: HPacket, category: int) -> list[int | str]: 294 | stuff = [] 295 | cat2 = category & 0xFF 296 | 297 | if cat2 == 0: # legacy 298 | stuff.append(packet.read_string()) 299 | if cat2 == 1: # map 300 | stuff.append([packet.read('ss') for _ in range(packet.read_int())]) 301 | if cat2 == 2: # string array 302 | stuff.append([packet.read_string() for _ in range(packet.read_int())]) 303 | if cat2 == 3: # vote results 304 | stuff.extend(packet.read('si')) 305 | if cat2 == 5: # int array 306 | stuff.append([packet.read_int() for _ in range(packet.read_int())]) 307 | if cat2 == 6: # highscores 308 | stuff.extend(packet.read('sii')) 309 | stuff.append([(packet.read_int(), [packet.read_string() for _ in range(packet.read_int())]) for _ in 310 | range(packet.read_int())]) 311 | if cat2 == 7: # crackables 312 | stuff.extend(packet.read('sii')) 313 | 314 | if (category & 0xFF00 & 0x100) > 0: 315 | stuff.extend(packet.read('ii')) 316 | 317 | return stuff 318 | 319 | 320 | def get_tile_from_coords(x: int, y: int, z: float) -> HPoint: 321 | try: 322 | z = float(z) 323 | except ValueError: 324 | z = 0.0 325 | 326 | return HPoint(x, y, z) 327 | 328 | 329 | class HFloorItem: 330 | def __init__(self, packet: HPacket): 331 | self.id, self.type_id, x, y, facing_id, z = packet.read('iiiiis') 332 | self.tile = HPoint(x, y, float(z)) 333 | self.facing = HDirection(facing_id) 334 | 335 | h, _, self.category = packet.read('sii') 336 | self.height = float(h) 337 | self.stuff = read_stuff(packet, self.category) 338 | 339 | self.seconds_to_expiration, self.usage_policy, self.owner_id = packet.read('iii') 340 | self.owner = None # expected to be filled in by parse class method 341 | 342 | if self.type_id < 0: 343 | packet.read_string() 344 | 345 | @classmethod 346 | def parse(cls, packet: HPacket) -> list[Self]: 347 | owners = {} 348 | for _ in range(packet.read_int()): 349 | owner_id = packet.read_int() 350 | owners[owner_id] = packet.read_string() 351 | 352 | furnis = [HFloorItem(packet) for _ in range(packet.read_int())] 353 | for furni in furnis: 354 | furni.owner = owners[furni.owner_id] 355 | 356 | return furnis 357 | 358 | 359 | class HGroup: 360 | def __init__(self, packet: HPacket): 361 | self.id, self.name, self.badge_code, self.primary_color, self.secondary_color, \ 362 | self.is_favorite, self.owner_id, self.has_forum = packet.read('issssBiB') 363 | 364 | def __str__(self) -> str: 365 | return f"id: {self.id}, name: {self.name}, badge_code: {self.badge_code}, " \ 366 | f"primary_color: {self.primary_color}, secondary_color: {self.secondary_color}, " \ 367 | f"is_favorite: {self.is_favorite}, owner_id: {self.owner_id}, has_forum: {self.has_forum}" 368 | 369 | 370 | class HUserProfile: 371 | def __init__(self, packet: HPacket): 372 | self.id, self.username, self.figure, self.motto, self.creation_date, self.achievement_score, \ 373 | self.friend_count, self.is_friend, self.is_requested_friend, self.is_online = packet.read('issssiiBBB') 374 | 375 | self.groups = [HGroup(packet) for _ in range(packet.read_int())] 376 | self.last_access_since, self.open_profile = packet.read('iB') 377 | 378 | self.idk1, self.level, self.idk2, self.gems, self.idk3, self.idk4 = packet.read('BiiiBB') 379 | 380 | def __str__(self) -> str: 381 | return "id: {}, username: {}, score: {}, friends: {}, online: {}, groups: {}, level: {}, gems: {}".format( 382 | self.id, self.username, self.achievement_score, 383 | self.friend_count, self.is_online, len(self.groups), self.level, self.gems) 384 | 385 | 386 | class HWallItem: 387 | def __init__(self, packet: HPacket): 388 | self.id, self.type_id, self.location, self.state, self.seconds_to_expiration, self.usage_policy, \ 389 | self.owner_id = packet.read('sissiii') 390 | 391 | @classmethod 392 | def parse(cls, packet: HPacket) -> list[Self]: 393 | owners = {} 394 | for _ in range(packet.read_int()): 395 | owner_id = packet.read_int() 396 | owners[owner_id] = packet.read_string() 397 | 398 | furnis = [HWallItem(packet) for _ in range(packet.read_int())] 399 | for furni in furnis: 400 | furni.owner = owners[furni.owner_id] 401 | 402 | return furnis 403 | 404 | 405 | class HWallUpdate: 406 | """ 407 | def update(p): 408 | wall = HWallUpdate(p.packet) 409 | print(wall.widthX, wall.widthY, wall.lengthX, wall.lengthY) 410 | # id / cord / rotation / widthX / widthY / lengthX / lengthY 411 | 412 | ext.intercept(Direction.TO_SERVER, update, 'MoveWallItem') 413 | """ 414 | 415 | def __init__(self, packet: HPacket): 416 | self.id, self.cord = packet.read('is') 417 | 418 | self.cord = self.cord.split() 419 | self.rotation = self.cord[2] 420 | self.widthX, self.widthY = self.cord[0].split(',') 421 | self.widthX = self.widthX.split('=')[1] 422 | self.lengthX, self.lengthY = self.cord[1].split(',') 423 | self.lengthX = self.lengthX.split('=')[1] 424 | 425 | 426 | class HInventoryItem: 427 | def __init__(self, packet: HPacket): 428 | _, test = packet.read('is') 429 | self.is_floor_furni = (test == 'S') 430 | 431 | self.id, self.type_id, special_type_id, self.category = packet.read('iiii') 432 | self.special_type = HSpecialType(special_type_id) 433 | self.stuff = read_stuff(packet, self.category) 434 | 435 | self.is_recyclable, self.is_tradeable, self.is_groupable, self.market_place_allowed, \ 436 | self.seconds_to_expiration, self.has_rent_period_started, self.room_Id = packet.read('BBBBiBi') 437 | 438 | if self.is_floor_furni: 439 | self.slot_id = packet.read_string() 440 | self.extra = packet.read_int() 441 | 442 | @classmethod 443 | def parse(cls, packet: HPacket) -> list[Self]: 444 | _total, _current = packet.read('ii') 445 | return [HInventoryItem(packet) for _ in range(packet.read_int())] 446 | 447 | 448 | class HNavigatorSearchResult: 449 | def __init__(self, packet: HPacket): 450 | self.search_code, self.filtering_data = packet.read('ss') 451 | 452 | self.blocks = [self.HNavigatorBlock(packet) for _ in range(packet.read_int())] 453 | 454 | class HNavigatorBlock: 455 | def __init__(self, packet: HPacket): 456 | self.search_code, self.text, self.action_allowed, self.is_force_closed, self.view_mode = packet.read( 457 | 'ssiBi') 458 | 459 | self.rooms = [self.HNavigatorRoom(packet) for _ in range(packet.read_int())] 460 | 461 | class HNavigatorRoom: 462 | def __init__(self, packet: HPacket): 463 | self.flat_id, self.room_name, self.owner_id, self.owner_name, door_mode_id, self.user_count, \ 464 | self.max_user_count, self.description, self.trade_mode, self.score, self.ranking, self.category_id \ 465 | = packet.read('isisiiisiiii') 466 | 467 | self.door_mode = HDoorMode(door_mode_id) 468 | 469 | self.tags = [packet.read_string() for _ in range(packet.read_int())] 470 | 471 | multi_use = packet.read_int() 472 | 473 | if (multi_use & 1) > 0: 474 | self.official_room_pic_ref = packet.read_string() 475 | 476 | if (multi_use & 2) > 0: 477 | self.group_id, self.group_name, self.group_badge_code = packet.read('iss') 478 | 479 | if (multi_use & 4) > 0: 480 | self.room_ad_name, self.room_ad_description, self.room_ad_expires_in_min = packet.read('ssi') 481 | 482 | self.show_owner = (multi_use & 8) > 0 483 | self.allow_pets = (multi_use & 16) > 0 484 | self.display_room_entry_ad = (multi_use & 32) > 0 485 | 486 | def __str__(self) -> str: 487 | return "id: {}, roomname: {}, door_mode: {}, users: {}/{}, description: {}".format( 488 | self.flat_id, self.room_name, self.door_mode.name, self.user_count, self.max_user_count, 489 | self.description) 490 | 491 | 492 | class HHeightMapTile(TypedDict): 493 | x: int 494 | y: int 495 | tile_value: int 496 | is_room_tile: bool 497 | tile_height: float 498 | is_stacking_blocked: bool 499 | 500 | 501 | class HHeightMap: 502 | def __init__(self, packet: HPacket): 503 | self.width, tileCount = packet.read('ii') 504 | self.height = int(tileCount / self.width) 505 | self.tiles = [packet.read_short() for _ in range(tileCount)] 506 | 507 | def coords_to_index(self, x: int, y: int) -> int: 508 | return int(y * self.width + x) 509 | 510 | def index_to_coords(self, index: int) -> (int, int): 511 | y = int(index % self.width) 512 | x = int((index - y) / self.width) 513 | return x, y 514 | 515 | def get_tile_value(self, x: int, y: int) -> int: 516 | return self.tiles[self.coords_to_index(x, y)] 517 | 518 | def are_valid_coords(self, x: int, y: int) -> bool: 519 | return 0 <= x < self.width and 0 <= y < self.height 520 | 521 | def get_tile_height(self, x: int, y: int) -> float: 522 | if not self.are_valid_coords(x, y): 523 | return -1 524 | value = self.get_tile_value(x, y) 525 | if value < 0: 526 | return -1 527 | return (value & 16383) / 256 528 | 529 | def is_room_tile(self, x: int, y: int) -> bool: 530 | if not self.are_valid_coords(x, y): 531 | return False 532 | value = self.get_tile_value(x, y) 533 | return value >= 0 534 | 535 | def is_stacking_blocked(self, x: int, y: int) -> bool: 536 | if not self.are_valid_coords(x, y): 537 | return False 538 | value = self.get_tile_value(x, y) 539 | return (value & 16384) > 0 540 | 541 | def get_tile(self, x: int, y: int) -> HHeightMapTile: 542 | return { 543 | 'x': x, 544 | 'y': y, 545 | 'tile_value': self.get_tile_value(x, y), 546 | 'is_room_tile': self.is_room_tile(x, y), 547 | 'tile_height': self.get_tile_height(x, y), 548 | 'is_stacking_blocked': self.is_stacking_blocked(x, y) 549 | } 550 | 551 | def get_tiles(self) -> list[HHeightMapTile]: 552 | return [self.get_tile(*self.index_to_coords(index)) for index in range(len(self.tiles))] 553 | -------------------------------------------------------------------------------- /g_python/gextension.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import socket 3 | import sys 4 | import threading 5 | from enum import IntEnum, StrEnum 6 | from typing import TypedDict, NotRequired, Callable 7 | 8 | from .hpacket import HPacket 9 | from .hmessage import HMessage, Direction 10 | 11 | MINIMUM_GEARTH_VERSION: str = "1.4.1" 12 | 13 | 14 | class IncomingMessages(IntEnum): 15 | ON_DOUBLE_CLICK = 1 16 | INFO_REQUEST = 2 17 | PACKET_INTERCEPT = 3 18 | FLAGS_CHECK = 4 19 | CONNECTION_START = 5 20 | CONNECTION_END = 6 21 | PACKET_TO_STRING_RESPONSE = 20 22 | STRING_TO_PACKET_RESPONSE = 21 23 | INIT = 7 24 | 25 | 26 | class OutgoingMessages(IntEnum): 27 | EXTENSION_INFO = 1 28 | MANIPULATED_PACKET = 2 29 | REQUEST_FLAGS = 3 30 | SEND_MESSAGE = 4 31 | PACKET_TO_STRING_REQUEST = 20 32 | STRING_TO_PACKET_REQUEST = 21 33 | EXTENSION_CONSOLE_LOG = 98 34 | 35 | 36 | class InterceptMethod(StrEnum): 37 | DEFAULT = 'default' 38 | ASYNC = 'async' 39 | ASYNC_MODIFY = 'async_modify' 40 | 41 | 42 | class ConsoleColour(StrEnum): 43 | GREY = 'grey' 44 | LIGHTGREY = 'lightgrey' 45 | YELLOW = 'yellow' 46 | ORANGE = 'orange' 47 | WHITE = 'white' 48 | PURPLE = 'purple' 49 | BROWN = 'brown' 50 | PINK = 'pink' 51 | RED = 'red' 52 | BLACK = 'black' 53 | BLUE = 'blue' 54 | CYAN = 'cyan' 55 | GREEN = 'green' 56 | DARK_GREEN = 'darkergreen' 57 | 58 | 59 | PORT_FLAG: list[str] = ["--port", "-p"] 60 | FILE_FLAG: list[str] = ["--filename", "-f"] 61 | COOKIE_FLAG: list[str] = ["--auth-token", "-c"] 62 | 63 | 64 | class ExtensionInfo(TypedDict): 65 | title: str 66 | description: str 67 | version: str 68 | author: str 69 | 70 | 71 | class ExtensionSettings(TypedDict): 72 | use_click_trigger: NotRequired[bool] 73 | can_leave: NotRequired[bool] 74 | can_delete: NotRequired[bool] 75 | 76 | 77 | EXTENSION_SETTINGS_DEFAULT: ExtensionSettings = {"use_click_trigger": False, "can_leave": True, "can_delete": True} 78 | EXTENSION_INFO_REQUIRED_FIELDS = ["title", "description", "version", "author"] 79 | 80 | 81 | def fill_settings(settings: ExtensionSettings, defaults: ExtensionSettings): 82 | if settings is None: 83 | return defaults.copy() 84 | 85 | settings = settings.copy() 86 | for key, value in defaults.items(): 87 | if key not in settings or settings[key] is None: 88 | settings[key] = value 89 | 90 | return settings 91 | 92 | 93 | def get_argument(args: list[str], flags: list[str] | str): 94 | if type(flags) == str: 95 | flags = [flags] 96 | 97 | for potential_flag in flags: 98 | if potential_flag in args: 99 | index = args.index(potential_flag) 100 | if 0 <= index < len(args) - 1: 101 | return args[index + 1] 102 | 103 | return None 104 | 105 | 106 | def run_callbacks(callbacks: list[Callable[[], None]]) -> None: 107 | for func in callbacks: 108 | func() 109 | 110 | 111 | class Extension: 112 | def __init__(self, extension_info: ExtensionInfo, args: list[str], 113 | extension_settings: None | ExtensionSettings = None, silent: bool = False): 114 | if not silent: 115 | print("WARNING: This version of G-Python requires G-Earth >= {}".format(MINIMUM_GEARTH_VERSION), 116 | file=sys.stderr) 117 | print("abc") 118 | 119 | extension_settings = fill_settings(extension_settings, EXTENSION_SETTINGS_DEFAULT) 120 | 121 | if get_argument(args, PORT_FLAG) is None: 122 | raise Exception('Port was not specified (argument example: -p 9092)') 123 | 124 | for key in EXTENSION_INFO_REQUIRED_FIELDS: 125 | if key not in extension_info: 126 | raise Exception('Extension info error: {} field missing'.format(key)) 127 | 128 | port = int(get_argument(args, PORT_FLAG)) 129 | file = get_argument(args, FILE_FLAG) 130 | cookie = get_argument(args, COOKIE_FLAG) 131 | 132 | self.__sock = None 133 | self.__lost_packets = 0 134 | 135 | self._extension_info = extension_info 136 | self.__port = port 137 | self.__file = file 138 | self.__cookie = cookie 139 | self._extension_settings = extension_settings 140 | 141 | self.connection_info = None 142 | self.packet_infos = None 143 | 144 | self.__start_barrier = threading.Barrier(2) 145 | self.__start_lock = threading.Lock() 146 | self.__stream_lock = threading.Lock() 147 | 148 | self.__events = {} 149 | self.__intercept_listeners = {Direction.TO_CLIENT: {-1: []}, Direction.TO_SERVER: {-1: []}} 150 | 151 | self.__request_lock = threading.Lock() 152 | self.__response_barrier = threading.Barrier(2) 153 | self.__response = None 154 | 155 | self.__manipulation_lock = threading.Lock() 156 | self.__manipulation_event = threading.Event() 157 | self.__manipulate_messages = [] 158 | 159 | def __read_gearth_packet(self) -> HPacket: 160 | write_pos = 0 161 | 162 | length_buffer = bytearray(4) 163 | while write_pos < 4: 164 | n_read = self.__sock.recv_into(memoryview(length_buffer)[write_pos:]) 165 | if n_read == 0: 166 | raise EOFError 167 | write_pos += n_read 168 | 169 | packet_length = int.from_bytes(length_buffer, byteorder='big') 170 | packet_buffer = length_buffer + bytearray(packet_length) 171 | 172 | while write_pos < 4 + packet_length: 173 | n_read = self.__sock.recv_into(memoryview(packet_buffer)[write_pos:]) 174 | if n_read == 0: 175 | raise EOFError 176 | write_pos += n_read 177 | 178 | return HPacket.from_bytes(packet_buffer) 179 | 180 | def __packet_manipulation_thread(self) -> None: 181 | while not self.is_closed(): 182 | habbo_message = None 183 | while habbo_message is None and not self.is_closed(): 184 | if len(self.__manipulate_messages) > 0: 185 | self.__manipulation_lock.acquire() 186 | habbo_message = self.__manipulate_messages.pop(0) 187 | self.__manipulation_lock.release() 188 | self.__manipulation_event.clear() 189 | else: 190 | self.__manipulation_event.wait(0.002) 191 | self.__manipulation_event.clear() 192 | 193 | if self.is_closed(): 194 | return 195 | 196 | habbo_packet = habbo_message.packet 197 | habbo_packet.default_extension = self 198 | 199 | for func in self.__intercept_listeners[habbo_message.direction][-1]: 200 | func(habbo_message) 201 | habbo_packet.reset() 202 | 203 | header_id = habbo_packet.header_id() 204 | potential_intercept_ids = {header_id} 205 | if self.packet_infos is not None and header_id in self.packet_infos[habbo_message.direction]: 206 | for elem in self.packet_infos[habbo_message.direction][header_id]: 207 | if elem['Name'] is not None: 208 | potential_intercept_ids.add(elem['Name']) 209 | if elem['Hash'] is not None: 210 | potential_intercept_ids.add(elem['Hash']) 211 | 212 | for identifier in potential_intercept_ids: 213 | if identifier in self.__intercept_listeners[habbo_message.direction]: 214 | for func in self.__intercept_listeners[habbo_message.direction][identifier]: 215 | func(habbo_message) 216 | habbo_packet.reset() 217 | 218 | response_packet = HPacket(OutgoingMessages.MANIPULATED_PACKET.value) 219 | response_packet.append_string(repr(habbo_message), head=4, encoding='iso-8859-1') 220 | self.__send_to_stream(response_packet) 221 | 222 | def __connection_thread(self) -> None: 223 | t = threading.Thread(target=self.__packet_manipulation_thread) 224 | t.start() 225 | 226 | while not self.is_closed(): 227 | try: 228 | packet = self.__read_gearth_packet() 229 | except EOFError: 230 | if not self.is_closed(): 231 | self.stop() 232 | return 233 | 234 | message_type = IncomingMessages(packet.header_id()) 235 | if message_type == IncomingMessages.INFO_REQUEST: 236 | response = HPacket(OutgoingMessages.EXTENSION_INFO.value) 237 | response \ 238 | .append_string(self._extension_info['title']) \ 239 | .append_string(self._extension_info['author']) \ 240 | .append_string(self._extension_info['version']) \ 241 | .append_string(self._extension_info['description']) \ 242 | .append_bool(self._extension_settings['use_click_trigger']) \ 243 | .append_bool(self.__file is not None) \ 244 | .append_string('' if self.__file is None else self.__file) \ 245 | .append_string('' if self.__cookie is None else self.__cookie) \ 246 | .append_bool(self._extension_settings['can_leave']) \ 247 | .append_bool(self._extension_settings['can_delete']) 248 | 249 | self.__send_to_stream(response) 250 | 251 | elif message_type == IncomingMessages.CONNECTION_START: 252 | host, port, hotel_version, client_identifier, client_type = packet.read("sisss") 253 | self.__parse_packet_infos(packet) 254 | 255 | self.connection_info = {'host': host, 'port': port, 'hotel_version': hotel_version, 256 | 'client_identifier': client_identifier, 'client_type': client_type} 257 | 258 | self.__raise_event('connection_start') 259 | 260 | if self.__await_connect_packet: 261 | self.__await_connect_packet = False 262 | self.__start_barrier.wait() 263 | 264 | elif message_type == IncomingMessages.CONNECTION_END: 265 | self.__raise_event('connection_end') 266 | self.connection_info = None 267 | self.packet_infos = None 268 | 269 | elif message_type == IncomingMessages.FLAGS_CHECK: 270 | size = packet.read_int() 271 | flags = [packet.read_string() for _ in range(size)] 272 | self.__response = flags 273 | self.__response_barrier.wait() 274 | 275 | elif message_type == IncomingMessages.INIT: 276 | self.__raise_event('init') 277 | self.write_to_console( 278 | 'g_python extension "{}" sucessfully initialized'.format(self._extension_info['title']), 279 | ConsoleColour.GREEN, 280 | False 281 | ) 282 | 283 | self.__await_connect_packet = packet.read_bool() 284 | if not self.__await_connect_packet: 285 | self.__start_barrier.wait() 286 | 287 | elif message_type == IncomingMessages.ON_DOUBLE_CLICK: 288 | self.__raise_event('double_click') 289 | 290 | elif message_type == IncomingMessages.PACKET_INTERCEPT: 291 | habbo_msg_as_string = packet.read_string(head=4, encoding='iso-8859-1') 292 | habbo_message = HMessage.reconstruct_from_java(habbo_msg_as_string) 293 | self.__manipulation_lock.acquire() 294 | self.__manipulate_messages.append(habbo_message) 295 | self.__manipulation_lock.release() 296 | self.__manipulation_event.set() 297 | 298 | elif message_type == IncomingMessages.PACKET_TO_STRING_RESPONSE: 299 | string = packet.read_string(head=4, encoding='iso-8859-1') 300 | expression = packet.read_string(head=4, encoding='utf-8') 301 | self.__response = (string, expression) 302 | self.__response_barrier.wait() 303 | 304 | elif message_type == IncomingMessages.STRING_TO_PACKET_RESPONSE: 305 | packet_string = packet.read_string(head=4, encoding='iso-8859-1') 306 | self.__response = HPacket.reconstruct_from_java(packet_string) 307 | self.__response_barrier.wait() 308 | 309 | def __parse_packet_infos(self, packet: HPacket) -> None: 310 | incoming = {} 311 | outgoing = {} 312 | 313 | length = packet.read_int() 314 | for _ in range(length): 315 | header_id, hash_code, name, structure, is_outgoing, source = packet.read('isssBs') 316 | name = name if name != 'NULL' else None 317 | hash_code = hash_code if hash_code != 'NULL' else None 318 | structure = structure if structure != 'NULL' else None 319 | 320 | elem = {'Id': header_id, 'Name': name, 'Hash': hash_code, 'Structure': structure, 'Source': source} 321 | 322 | packet_dict = outgoing if is_outgoing else incoming 323 | if header_id not in packet_dict: 324 | packet_dict[header_id] = [] 325 | packet_dict[header_id].append(elem) 326 | 327 | if hash_code is not None: 328 | if hash_code not in packet_dict: 329 | packet_dict[hash_code] = [] 330 | packet_dict[hash_code].append(elem) 331 | 332 | if name is not None: 333 | if name not in packet_dict: 334 | packet_dict[name] = [] 335 | packet_dict[name].append(elem) 336 | 337 | self.packet_infos = {Direction.TO_CLIENT: incoming, Direction.TO_SERVER: outgoing} 338 | 339 | def __send_to_stream(self, packet: HPacket) -> None: 340 | self.__stream_lock.acquire() 341 | self.__sock.send(packet.bytearray) 342 | self.__stream_lock.release() 343 | 344 | def __raise_event(self, event_name: str) -> None: 345 | if event_name in self.__events: 346 | t = threading.Thread(target=run_callbacks, args=(self.__events[event_name],)) 347 | t.start() 348 | 349 | def __send(self, direction: Direction, packet: HPacket) -> bool: 350 | if not self.is_closed(): 351 | 352 | old_settings = None 353 | if packet.is_incomplete_packet(): 354 | old_settings = (packet.header_id(), packet.is_edited, packet.incomplete_identifier) 355 | packet.fill_id(direction, self) 356 | 357 | if self.connection_info is None: 358 | self.__lost_packets += 1 359 | print("Could not send packet because G-Earth isn't connected to a client", file=sys.stderr) 360 | return False 361 | 362 | if packet.is_corrupted(): 363 | self.__lost_packets += 1 364 | print('Could not send corrupted', file=sys.stderr) 365 | return False 366 | 367 | if packet.is_incomplete_packet(): 368 | self.__lost_packets += 1 369 | print('Could not send incomplete packet', file=sys.stderr) 370 | return False 371 | 372 | wrapper_packet = HPacket(OutgoingMessages.SEND_MESSAGE.value, direction == Direction.TO_SERVER, 373 | len(packet.bytearray), bytes(packet.bytearray)) 374 | self.__send_to_stream(wrapper_packet) 375 | 376 | if old_settings is not None: 377 | packet.replace_short(4, old_settings[0]) 378 | packet.incomplete_identifier = old_settings[2] 379 | packet.is_edited = old_settings[1] 380 | 381 | return True 382 | else: 383 | self.__lost_packets += 1 384 | return False 385 | 386 | def is_closed(self) -> bool: 387 | """ 388 | :return: true if no extension isn't connected with G-Earth 389 | """ 390 | return self.__sock is None or self.__sock.fileno() == -1 391 | 392 | def send_to_client(self, packet: HPacket | str) -> bool: 393 | """ 394 | Sends a message to the client 395 | :param packet: a HPacket() or a string representation 396 | """ 397 | 398 | if type(packet) is str: 399 | packet = self.string_to_packet(packet) 400 | return self.__send(Direction.TO_CLIENT, packet) 401 | 402 | def send_to_server(self, packet: HPacket | str) -> bool: 403 | """ 404 | Sends a message to the server 405 | :param packet: a HPacket() or a string representation 406 | """ 407 | 408 | if type(packet) is str: 409 | packet = self.string_to_packet(packet) 410 | return self.__send(Direction.TO_SERVER, packet) 411 | 412 | def on_event(self, event_name: str, func: Callable) -> None: 413 | """ 414 | implemented event names: double_click, connection_start, connection_end,init. When this 415 | even occurs, a callback is being done to "func" 416 | """ 417 | if event_name in self.__events: 418 | self.__events[event_name].append(func) 419 | else: 420 | self.__events[event_name] = [func] 421 | 422 | def intercept(self, direction: Direction, callback: Callable[[HMessage], None], identifier: int | str = -1, 423 | mode: InterceptMethod = InterceptMethod.DEFAULT) -> None: 424 | """ 425 | :param direction: Direction.TOCLIENT or Direction.TOSERVER 426 | :param callback: function that takes HMessage as an argument 427 | :param identifier: header_id / hash / name 428 | :param mode: can be: * default (blocking) 429 | * async (async, can't modify packet, doesn't disturb packet flow) 430 | * async_modify (async, can modify, doesn't block other packets, disturbs packet flow) 431 | :return: 432 | """ 433 | original_callback = callback 434 | 435 | if mode == 'async': 436 | def new_callback(hmessage: HMessage) -> None: 437 | copied = copy.copy(hmessage) 438 | t = threading.Thread(target=original_callback, args=[copied]) 439 | t.start() 440 | 441 | callback = new_callback 442 | 443 | if mode == 'async_modify': 444 | def callback_send(hmessage: HMessage) -> None: 445 | original_callback(hmessage) 446 | if not hmessage.is_blocked: 447 | self.__send(hmessage.direction, hmessage.packet) 448 | 449 | def new_callback(hmessage: HMessage) -> None: 450 | hmessage.is_blocked = True 451 | copied = copy.copy(hmessage) 452 | copied.is_blocked = False 453 | t = threading.Thread(target=callback_send, args=[copied]) 454 | t.start() 455 | 456 | callback = new_callback 457 | 458 | if identifier not in self.__intercept_listeners[direction]: 459 | self.__intercept_listeners[direction][identifier] = [] 460 | self.__intercept_listeners[direction][identifier].append(callback) 461 | 462 | def remove_intercept(self, intercept_id: int | str = -1) -> None: 463 | """ 464 | Clear intercepts per id or all of them when none is given 465 | """ 466 | 467 | if intercept_id == -1: 468 | for direction in self.__intercept_listeners: 469 | for identifier in self.__intercept_listeners[direction]: 470 | del self.__intercept_listeners[direction][identifier] 471 | else: 472 | for direction in self.__intercept_listeners: 473 | if intercept_id in self.__intercept_listeners[direction]: 474 | del self.__intercept_listeners[direction][intercept_id] 475 | 476 | def start(self) -> None: 477 | """ 478 | Tries to set up a connection with G-Earth 479 | """ 480 | self.__start_lock.acquire() 481 | if self.is_closed(): 482 | self.__sock = socket.socket() 483 | self.__sock.connect(("127.0.0.1", self.__port)) 484 | self.__sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 485 | t = threading.Thread(target=self.__connection_thread) 486 | t.start() 487 | self.__start_barrier.wait() 488 | else: 489 | self.__start_lock.release() 490 | raise Exception("Attempted to run already-running extension") 491 | self.__start_lock.release() 492 | 493 | def stop(self) -> None: 494 | """ 495 | Aborts an existing connection with G-Earth 496 | """ 497 | if not self.is_closed(): 498 | self.__sock.close() 499 | else: 500 | raise Exception("Attempted to close extension that wasn't running") 501 | 502 | def write_to_console(self, text, color: ConsoleColour = ConsoleColour.BLACK, mention_title: bool = True) -> None: 503 | """ 504 | Writes a message to the G-Earth console 505 | """ 506 | message = '[{}]{}{}'.format(color, (self._extension_info['title'] + ' --> ') if mention_title else '', text) 507 | packet = HPacket(OutgoingMessages.EXTENSION_CONSOLE_LOG.value, message) 508 | self.__send_to_stream(packet) 509 | 510 | def __await_response(self, request: HPacket) -> str | list[str] | HPacket: 511 | self.__request_lock.acquire() 512 | self.__send_to_stream(request) 513 | self.__response_barrier.wait() 514 | result = self.__response 515 | self.__response = None 516 | self.__request_lock.release() 517 | return result 518 | 519 | def packet_to_string(self, packet: HPacket) -> str: 520 | request = HPacket(OutgoingMessages.PACKET_TO_STRING_REQUEST.value) 521 | request.append_string(repr(packet), 4, 'iso-8859-1') 522 | 523 | return self.__await_response(request)[0] 524 | 525 | def packet_to_expression(self, packet: HPacket) -> str: 526 | request = HPacket(OutgoingMessages.PACKET_TO_STRING_REQUEST.value) 527 | request.append_string(repr(packet), 4, 'iso-8859-1') 528 | 529 | return self.__await_response(request)[1] 530 | 531 | def string_to_packet(self, string: str) -> HPacket: 532 | request = HPacket(OutgoingMessages.STRING_TO_PACKET_REQUEST.value) 533 | request.append_string(string, 4) 534 | 535 | return self.__await_response(request) 536 | 537 | def request_flags(self) -> list[str]: 538 | return self.__await_response(HPacket(OutgoingMessages.REQUEST_FLAGS.value)) 539 | --------------------------------------------------------------------------------