From 71b52d358946e473c03ff3cf932ad57b3ccad43f Mon Sep 17 00:00:00 2001 From: olijeffers0n <69084614+olijeffers0n@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:49:50 +0100 Subject: [PATCH] Its a start --- rustplus/__init__.py | 21 +- rustplus/identification/__init__.py | 2 + .../identification/registered_listener.py | 49 ++++ rustplus/identification/server_id.py | 26 ++ rustplus/remote/__init__.py | 0 rustplus/remote/camera/__init__.py | 3 + rustplus/remote/handler_list.py | 62 +++++ rustplus/remote/ratelimiter/__init__.py | 134 ++++++++++ rustplus/remote/websocket/__init__.py | 1 + rustplus/remote/websocket/ws.py | 248 ++++++++++++++++++ rustplus/rust_api.py | 241 +++++++++++++++++ rustplus/structs/__init__.py | 10 + rustplus/utils/__init__.py | 5 - 13 files changed, 779 insertions(+), 23 deletions(-) create mode 100644 rustplus/identification/__init__.py create mode 100644 rustplus/identification/registered_listener.py create mode 100644 rustplus/identification/server_id.py create mode 100644 rustplus/remote/__init__.py create mode 100644 rustplus/remote/camera/__init__.py create mode 100644 rustplus/remote/handler_list.py create mode 100644 rustplus/remote/ratelimiter/__init__.py create mode 100644 rustplus/remote/websocket/__init__.py create mode 100644 rustplus/remote/websocket/ws.py create mode 100644 rustplus/rust_api.py create mode 100644 rustplus/structs/__init__.py diff --git a/rustplus/__init__.py b/rustplus/__init__.py index ff6bce3..29b9264 100644 --- a/rustplus/__init__.py +++ b/rustplus/__init__.py @@ -2,25 +2,10 @@ RustPlus, An API wrapper for interfacing with the Rust+ App API """ -from .api import RustSocket -from .api.remote.events import ( - EntityEvent, - TeamEvent, - ChatEvent, - MarkerEvent, - ProtobufEvent, - RegisteredListener, -) -from .api.structures import RustMarker, Vector -from .api.remote.fcm_listener import FCMListener -from .api.remote.ratelimiter import RateLimiter -from .api.remote.camera import CameraManager, MovementControls, CameraMovementOptions -from .commands import CommandOptions, Command -from .exceptions import * -from .conversation import ConversationFactory, Conversation, ConversationPrompt -from .utils import * +from .rust_api import RustSocket +from .identification import ServerID __name__ = "rustplus" __author__ = "olijeffers0n" -__version__ = "5.6.18" +__version__ = "6.0.0" __support__ = "Discord: https://discord.gg/nQqJe8qvP8" diff --git a/rustplus/identification/__init__.py b/rustplus/identification/__init__.py new file mode 100644 index 0000000..d647994 --- /dev/null +++ b/rustplus/identification/__init__.py @@ -0,0 +1,2 @@ +from .registered_listener import RegisteredListener, RegisteredEntityListener +from .server_id import ServerID diff --git a/rustplus/identification/registered_listener.py b/rustplus/identification/registered_listener.py new file mode 100644 index 0000000..a7323ef --- /dev/null +++ b/rustplus/identification/registered_listener.py @@ -0,0 +1,49 @@ +from typing import Coroutine + + +class RegisteredListener: + def __init__( + self, + listener_id: str, + coroutine: Coroutine, + ) -> None: + self.listener_id = listener_id + self._coroutine = coroutine + + def get_coro(self): + return self._coroutine + + def __eq__(self, other) -> bool: + if not isinstance(other, RegisteredListener): + return False + + return ( + self.listener_id == other.listener_id + and self._coroutine == other.get_coro() + ) + + def __hash__(self): + return hash((self.listener_id, self._coroutine)) + + +class RegisteredEntityListener(RegisteredListener): + def __init__( + self, + listener_id: str, + coroutine: Coroutine, + entity_type: int, + ) -> None: + super().__init__(listener_id, coroutine) + self.entity_type = entity_type + + def get_entity_type(self): + return self.entity_type + + def __eq__(self, other) -> bool: + if not isinstance(other, RegisteredEntityListener): + return False + + return super().__eq__(other) and self.listener_id == other.listener_id + + def __hash__(self): + return hash((self.listener_id, self._coroutine, self.entity_type)) diff --git a/rustplus/identification/server_id.py b/rustplus/identification/server_id.py new file mode 100644 index 0000000..3639595 --- /dev/null +++ b/rustplus/identification/server_id.py @@ -0,0 +1,26 @@ +class ServerID: + def __init__(self, ip: str, port: str, player_id: int, player_token: int) -> None: + self.ip = ip + self.port = port + self.player_id = player_id + self.player_token = player_token + + def __str__(self) -> str: + return f"{self.ip}:{self.port} {self.player_id} {self.player_token}" + + def get_server_string(self) -> str: + return f"{self.ip}:{self.port}" + + def __hash__(self): + return hash(self.__str__()) + + def __eq__(self, o: object) -> bool: + if not isinstance(o, ServerID): + return False + + return ( + self.ip == o.ip + and self.port == o.port + and self.player_id == o.player_id + and self.player_token == o.player_token + ) \ No newline at end of file diff --git a/rustplus/remote/__init__.py b/rustplus/remote/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rustplus/remote/camera/__init__.py b/rustplus/remote/camera/__init__.py new file mode 100644 index 0000000..b39ec70 --- /dev/null +++ b/rustplus/remote/camera/__init__.py @@ -0,0 +1,3 @@ +from .camera_manager import CameraManager +from .camera_constants import CameraMovementOptions, MovementControls +from .structures import CameraInfo diff --git a/rustplus/remote/handler_list.py b/rustplus/remote/handler_list.py new file mode 100644 index 0000000..ad72833 --- /dev/null +++ b/rustplus/remote/handler_list.py @@ -0,0 +1,62 @@ +from collections import defaultdict +from typing import Set, Dict +from ..identification import RegisteredListener, RegisteredEntityListener, ServerID + + +class HandlerList: + def __init__(self) -> None: + self._handlers: Dict[ServerID, Set[RegisteredListener]] = defaultdict(set) + + def unregister(self, listener: RegisteredListener, server_id: ServerID) -> None: + self._handlers[server_id].remove(listener) + + def register(self, listener: RegisteredListener, server_id: ServerID) -> None: + self._handlers[server_id].add(listener) + + def has(self, listener: RegisteredListener, server_id: ServerID) -> bool: + return listener in self._handlers[server_id] + + def unregister_all(self) -> None: + self._handlers.clear() + + def get_handlers( + self, server_id: ServerID + ) -> Set[RegisteredListener]: + return self._handlers.get(server_id, set()) + + +class EntityHandlerList(HandlerList): + def __init__(self) -> None: + super().__init__() + self._handlers: Dict[ServerID, Dict[str, Set[RegisteredEntityListener]]] = ( + defaultdict(dict) + ) + + def unregister(self, listener: RegisteredEntityListener, server_id: ServerID) -> None: + if listener.listener_id in self._handlers.get(server_id): + self._handlers.get(server_id).get(listener.listener_id).remove(listener) + + def register(self, listener: RegisteredEntityListener, server_id: ServerID) -> None: + if server_id not in self._handlers: + self._handlers[server_id] = defaultdict(set) + + if listener.listener_id not in self._handlers.get(server_id): + self._handlers.get(server_id)[listener.listener_id] = set() + + self._handlers.get(server_id).get(listener.listener_id).add(listener) + + def has(self, listener: RegisteredEntityListener, server_id: ServerID) -> bool: + if server_id in self._handlers and listener.listener_id in self._handlers.get( + server_id + ): + return listener in self._handlers.get(server_id).get(listener.listener_id) + + return False + + def unregister_all(self) -> None: + self._handlers.clear() + + def get_handlers( + self, server_id: ServerID + ) -> Dict[str, Set[RegisteredEntityListener]]: + return self._handlers.get(server_id, dict()) diff --git a/rustplus/remote/ratelimiter/__init__.py b/rustplus/remote/ratelimiter/__init__.py new file mode 100644 index 0000000..df0630d --- /dev/null +++ b/rustplus/remote/ratelimiter/__init__.py @@ -0,0 +1,134 @@ +import math +import time +import asyncio +from typing import Dict + +from ...exceptions.exceptions import RateLimitError +from ...identification import ServerID + + +class TokenBucket: + def __init__( + self, current: float, maximum: float, refresh_rate: float, refresh_amount: float + ) -> None: + self.current = current + self.max = maximum + self.refresh_rate = refresh_rate + self.refresh_amount = refresh_amount + self.last_update = time.time() + self.refresh_per_second = self.refresh_amount / self.refresh_rate + + def can_consume(self, amount) -> bool: + if (self.current - amount) >= 0: + return True + + return False + + def consume(self, amount: int = 1) -> None: + self.current -= amount + + def refresh(self) -> None: + time_now = time.time() + + time_delta = time_now - self.last_update + self.last_update = time_now + + self.current = min([self.current + time_delta * self.refresh_amount, self.max]) + + +class RateLimiter: + SERVER_LIMIT = 50 + SERVER_REFRESH_AMOUNT = 15 + + @classmethod + def default(cls) -> "RateLimiter": + """ + Returns a default rate limiter with 3 tokens per second + """ + return cls() + + def __init__(self) -> None: + self.socket_buckets: Dict[ServerID, TokenBucket] = {} + self.server_buckets: Dict[str, TokenBucket] = {} + self.lock = asyncio.Lock() + + def add_socket( + self, + server_id: ServerID, + current: float, + maximum: float, + refresh_rate: float, + refresh_amount: float, + ) -> None: + self.socket_buckets[server_id] = TokenBucket( + current, maximum, refresh_rate, refresh_amount + ) + if server_id.get_server_string() not in self.server_buckets: + self.server_buckets[server_id.get_server_string()] = TokenBucket( + self.SERVER_LIMIT, self.SERVER_LIMIT, 1, self.SERVER_REFRESH_AMOUNT + ) + + async def can_consume(self, server_id: ServerID, amount: int = 1) -> bool: + """ + Returns whether the user can consume the amount of tokens provided + """ + async with self.lock: + can_consume = True + + for bucket in [ + self.socket_buckets.get(server_id), + self.server_buckets.get(server_id.get_server_string()), + ]: + bucket.refresh() + if not bucket.can_consume(amount): + can_consume = False + + return can_consume + + async def consume(self, server_id: ServerID, amount: int = 1) -> None: + """ + Consumes an amount of tokens from the bucket. You should first check to see whether it is possible with can_consume + """ + async with self.lock: + for bucket in [ + self.socket_buckets.get(server_id), + self.server_buckets.get(server_id.get_server_string()), + ]: + bucket.refresh() + if not bucket.can_consume(amount): + self.lock.release() + raise RateLimitError("Not Enough Tokens") + bucket.consume(amount) + + async def get_estimated_delay_time( + self, server_id: ServerID, target_cost: int + ) -> float: + """ + Returns how long until the amount of tokens needed will be available + """ + async with self.lock: + delay = 0 + for bucket in [ + self.socket_buckets.get(server_id), + self.server_buckets.get(server_id.get_server_string()), + ]: + val = ( + math.ceil( + ( + ((target_cost - bucket.current) / bucket.refresh_per_second) + + 0.1 + ) + * 100 + ) + / 100 + ) + if val > delay: + delay = val + return delay + + async def remove(self, server_id: ServerID) -> None: + """ + Removes the limiter + """ + async with self.lock: + del self.socket_buckets[server_id] \ No newline at end of file diff --git a/rustplus/remote/websocket/__init__.py b/rustplus/remote/websocket/__init__.py new file mode 100644 index 0000000..bc2f0a5 --- /dev/null +++ b/rustplus/remote/websocket/__init__.py @@ -0,0 +1 @@ +from .ws import RustWebsocket diff --git a/rustplus/remote/websocket/ws.py b/rustplus/remote/websocket/ws.py new file mode 100644 index 0000000..54d34c2 --- /dev/null +++ b/rustplus/remote/websocket/ws.py @@ -0,0 +1,248 @@ +import base64 + +import betterproto +from websockets.exceptions import InvalidURI, InvalidHandshake +from websockets.legacy.client import WebSocketClientProtocol +from websockets.client import connect +from asyncio import TimeoutError, Task, AbstractEventLoop +from typing import Union, Coroutine, Optional, Set, Dict +import logging +import asyncio + +from ..rustplus_proto import AppMessage, AppRequest +from ...exceptions import ClientNotConnectedError +from ...identification import ServerID +from ...structs import RustChatMessage +from ...utils.yielding_event import YieldingEvent + + +class RustWebsocket: + def __init__(self, server_id: ServerID) -> None: + self.server_id: ServerID = server_id + self.connection: Union[WebSocketClientProtocol, None] = None + self.logger: logging.Logger = logging.getLogger("rustplus.py") + self.task: Union[Task, None] = None + self.debug: bool = True + self.use_test_server: bool = True + + self.responses_to_ignore: Set[int] = set() + self.responses: Dict[int, YieldingEvent] = {} + + async def connect(self) -> bool: + + if self.use_test_server: + address = "wss://" + self.server_id.ip + else: + address = "ws://" + self.server_id.get_server_string() + + try: + self.connection = await connect( + address, + close_timeout=0, + ping_interval=None, + max_size=1_000_000_000, + ) + except (InvalidURI, OSError, InvalidHandshake, TimeoutError) as err: + self.logger.warning("WebSocket connection error: %s", err) + return False + + self.logger.info("Websocket connection established to %s", address) + + self.task = asyncio.create_task( + self.run(), name="[RustPlus.py] Websocket Polling Task" + ) + + return True + + async def run(self) -> None: + while True: + try: + data = await self.connection.recv() + + # TODO + # await self.run_coroutine_non_blocking( + # EventHandler.run_proto_event(data, self.server_id) + # ) + + app_message = AppMessage() + app_message.parse(data) + + except Exception as e: + continue + + try: + await self.run_coroutine_non_blocking(self.handle_message(app_message)) + except Exception as e: + print(e) + self.logger.exception( + "An Error occurred whilst handling the message from the server" + ) + + async def send_message(self, request: AppRequest) -> None: + if self.connection is None: + raise ClientNotConnectedError("No Current Websocket Connection") + + if self.debug: + self.logger.info( + f"[RustPlus.py] Sending Message with seq {request.seq}: {request}" + ) + + self.responses[request.seq] = YieldingEvent() + + try: + if self.use_test_server: + await self.connection.send( + base64.b64encode(bytes(request)).decode("utf-8") + ) + else: + await self.connection.send(bytes(request)) + except Exception: + self.logger.exception("An exception occurred whilst sending a message") + + async def handle_message(self, app_message: AppMessage) -> None: + if self.debug: + self.logger.info( + f"[RustPlus.py] Received Message with seq {app_message.response.seq}: {app_message}" + ) + + if app_message.response.seq in self.responses_to_ignore: + self.responses_to_ignore.remove(app_message.response.seq) + return + + prefix = self.get_prefix( + str(app_message.broadcast.team_message.message.message) + ) + + if prefix is not None: + # This means it is a command + + if self.debug: + self.logger.info( + f"[RustPlus.py] Attempting to run Command: {app_message}" + ) + + message = RustChatMessage(app_message.broadcast.team_message.message) + # TODO await self.remote.command_handler.run_command(message, prefix) + + if self.is_entity_broadcast(app_message): + # This means that an entity has changed state + + if self.debug: + self.logger.info(f"[RustPlus.py] Running Entity Event: {app_message}") + + # TODO + # await EventHandler.run_entity_event( + # app_message.broadcast.entity_changed.entity_id, + # app_message, + # self.server_id, + # ) + + elif self.is_camera_broadcast(app_message): + if self.debug: + self.logger.info(f"[RustPlus.py] Running Camera Event: {app_message}") + + # TODO + # if self.remote.camera_manager is not None: + # await self.remote.camera_manager.add_packet( + # RayPacket(app_message.broadcast.camera_rays) + # ) + + elif self.is_team_broadcast(app_message): + if self.debug: + self.logger.info(f"[RustPlus.py] Running Team Event: {app_message}") + + # This means that the team of the current player has changed + # TODO await EventHandler.run_team_event(app_message, self.server_id) + + elif self.is_message(app_message): + # This means that a message has been sent to the team chat + + if self.debug: + self.logger.info(f"[RustPlus.py] Running Chat Event: {app_message}") + + steam_id = int(app_message.broadcast.team_message.message.steam_id) + message = str(app_message.broadcast.team_message.message.message) + + # TODO await EventHandler.run_chat_event(app_message, self.server_id) + + else: + # This means that it wasn't sent by the server and is a message from the server in response to an action + event: YieldingEvent = self.responses.get( + app_message.response.seq, None + ) + if event is not None: + if self.debug: + self.logger.info( + f"[RustPlus.py] Running Response Event: {app_message}" + ) + + event.set_with_value(app_message) + + def get_prefix(self, message: str) -> Optional[str]: + + return None + + if self.remote.use_commands: + if message.startswith(self.remote.command_options.prefix): + return self.remote.command_options.prefix + else: + return None + + for overrule in self.remote.command_options.overruling_commands: + if message.startswith(overrule): + return overrule + + return None + + @staticmethod + def is_message(app_message: AppMessage) -> bool: + return betterproto.serialized_on_wire( + app_message.broadcast.team_message.message + ) + + @staticmethod + def is_camera_broadcast(app_message: AppMessage) -> bool: + return betterproto.serialized_on_wire(app_message.broadcast.camera_rays) + + @staticmethod + def is_entity_broadcast(app_message: AppMessage) -> bool: + return betterproto.serialized_on_wire(app_message.broadcast.entity_changed) + + @staticmethod + def is_team_broadcast(app_message: AppMessage) -> bool: + return betterproto.serialized_on_wire(app_message.broadcast.team_changed) + + @staticmethod + def get_proto_cost(app_request: AppRequest) -> int: + """ + Gets the cost of an AppRequest + """ + costs = [ + (app_request.get_time, 1), + (app_request.send_team_message, 2), + (app_request.get_info, 1), + (app_request.get_team_chat, 1), + (app_request.get_team_info, 1), + (app_request.get_map_markers, 1), + (app_request.get_map, 5), + (app_request.set_entity_value, 1), + (app_request.get_entity_info, 1), + (app_request.promote_to_leader, 1), + ] + for request, cost in costs: + if betterproto.serialized_on_wire(request): + return cost + + raise ValueError() + + @staticmethod + def error_present(message) -> bool: + """ + Checks message for error + """ + return message != "" + + @staticmethod + async def run_coroutine_non_blocking(coroutine: Coroutine) -> Task: + loop: AbstractEventLoop = asyncio.get_event_loop_policy().get_event_loop() + return loop.create_task(coroutine) diff --git a/rustplus/rust_api.py b/rustplus/rust_api.py new file mode 100644 index 0000000..1514088 --- /dev/null +++ b/rustplus/rust_api.py @@ -0,0 +1,241 @@ +import asyncio +from typing import List, Union +import logging +from PIL import Image + +from .exceptions import RateLimitError +from .identification import ServerID +from .remote.camera import CameraManager +from .remote.rustplus_proto import AppRequest, AppEmpty +from .remote.websocket import RustWebsocket +from .structs import RustTime, RustInfo, RustChatMessage, RustTeamInfo, RustMarker, RustMap, RustEntityInfo, \ + RustContents +from .utils import deprecated +from .remote.ratelimiter import RateLimiter + + +class RustSocket: + + def __init__(self, server_id: ServerID, ratelimiter: Union[None, RateLimiter] = None) -> None: + self.server_id = server_id + self.logger = logging.getLogger("rustplus.py") + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + self.logger.addHandler(console_handler) + self.logger.setLevel(logging.DEBUG) + + self.ws = RustWebsocket(self.server_id) + self.seq = 1 + + if ratelimiter: + self.ratelimiter = ratelimiter + else: + self.ratelimiter = RateLimiter() + + self.ratelimiter.add_socket(self.server_id, RateLimiter.SERVER_LIMIT, RateLimiter.SERVER_LIMIT, 1, + RateLimiter.SERVER_REFRESH_AMOUNT) + + async def __generate_request(self, tokens=1) -> AppRequest: + while True: + if await self.ratelimiter.can_consume(self.server_id, tokens): + await self.ratelimiter.consume(self.server_id, tokens) + break + + if False: # TODO self.raise_ratelimit_exception: + raise RateLimitError("Out of tokens") + + await asyncio.sleep( + await self.ratelimiter.get_estimated_delay_time( + self.server_id, tokens + ) + ) + + app_request = AppRequest() + app_request.seq = self.seq + self.seq += 1 + app_request.player_id = self.server_id.player_id + app_request.player_tokens = self.server_id.player_token + + return app_request + + async def connect(self) -> None: + await self.ws.connect() + + @staticmethod + async def hang() -> None: + """ + This Will permanently put your script into a state of 'hanging' Cannot be Undone. Only do this in scripts + using commands + + :returns Nothing, This will never return + """ + + while True: + await asyncio.sleep(1) + + async def get_time(self) -> RustTime: + """ + Gets the current in-game time from the server. + + :returns RustTime: The Time + """ + + packet = await self.__generate_request() + packet.get_time = AppEmpty() + + await self.ws.send_message(packet) + return None + + async def send_team_message(self, message: str) -> None: + """ + Sends a message to the in-game team chat + + :param message: The string message to send + """ + raise NotImplementedError("Not Implemented") + + async def get_info(self) -> RustInfo: + """ + Gets information on the Rust Server + :return: RustInfo - The info of the server + """ + raise NotImplementedError("Not Implemented") + + async def get_team_chat(self) -> List[RustChatMessage]: + """ + Gets the team chat from the server + + :return List[RustChatMessage]: The chat messages in the team chat + """ + raise NotImplementedError("Not Implemented") + + async def get_team_info(self) -> RustTeamInfo: + """ + Gets Information on the members of your team + + :return RustTeamInfo: The info of your team + """ + raise NotImplementedError("Not Implemented") + + async def get_markers(self) -> List[RustMarker]: + """ + Gets all the map markers from the server + + :return List[RustMarker]: All the markers on the map + """ + raise NotImplementedError("Not Implemented") + + async def get_map( + self, + add_icons: bool = False, + add_events: bool = False, + add_vending_machines: bool = False, + override_images: dict = None, + add_grid: bool = False, + ) -> Image.Image: + """ + Gets an image of the map from the server with the specified additions + + :param add_icons: To add the monument icons + :param add_events: To add the Event icons + :param add_vending_machines: To add the vending icons + :param override_images: To override the images pre-supplied with RustPlus.py + :param add_grid: To add the grid to the map + :return Image: PIL Image + """ + raise NotImplementedError("Not Implemented") + + async def get_raw_map_data(self) -> RustMap: + """ + Gets the raw map data from the server + + :return RustMap: The raw map of the server + """ + raise NotImplementedError("Not Implemented") + + async def get_entity_info(self, eid: int = None) -> RustEntityInfo: + """ + Gets entity info from the server + + :param eid: The Entities ID + :return RustEntityInfo: The entity Info + """ + raise NotImplementedError("Not Implemented") + + async def turn_on_smart_switch(self, eid: int = None) -> None: + """ + Turns on a given smart switch by entity ID + + :param eid: The Entities ID + :return None: + """ + raise NotImplementedError("Not Implemented") + + async def turn_off_smart_switch(self, eid: int = None) -> None: + """ + Turns off a given smart switch by entity ID + + :param eid: The Entities ID + :return None: + """ + raise NotImplementedError("Not Implemented") + + async def promote_to_team_leader(self, steamid: int = None) -> None: + """ + Promotes a given user to the team leader by their 64-bit Steam ID + + :param steamid: The SteamID of the player to promote + :return None: + """ + raise NotImplementedError("Not Implemented") + + @deprecated("Use RustSocket#get_markers") + async def get_current_events(self) -> List[RustMarker]: + """ + Returns all the map markers that are for events: + Can detect: + - Explosion + - CH47 (Chinook) + - Cargo Ship + - Locked Crate + - Attack Helicopter + + :return List[RustMarker]: All current events + """ + raise NotImplementedError("Not Implemented") + + async def get_contents( + self, eid: int = None, combine_stacks: bool = False + ) -> RustContents: + """ + Gets the contents of a storage monitor-attached container + + :param eid: The EntityID Of the storage Monitor + :param combine_stacks: Whether to combine alike stacks together + :return RustContents: The contents on the monitor + """ + raise NotImplementedError("Not Implemented") + + @deprecated("Use RustSocket#get_contents") + async def get_tc_storage_contents( + self, eid: int = None, combine_stacks: bool = False + ) -> RustContents: + """ + Gets the Information about TC Upkeep and Contents. + Do not use this for any other storage monitor than a TC + """ + raise NotImplementedError("Not Implemented") + + async def get_camera_manager(self, cam_id: str) -> CameraManager: + """ + Gets a camera manager for a given camera ID + + NOTE: This will override the current camera manager if one exists for the given ID so you cannot have multiple + + :param cam_id: The ID of the camera + :return CameraManager: The camera manager + :raises RequestError: If the camera is not found, or you cannot access it. See reason for more info + """ + raise NotImplementedError("Not Implemented") diff --git a/rustplus/structs/__init__.py b/rustplus/structs/__init__.py new file mode 100644 index 0000000..5ac6b8f --- /dev/null +++ b/rustplus/structs/__init__.py @@ -0,0 +1,10 @@ +from .rust_time import RustTime +from .rust_info import RustInfo +from .rust_map import RustMap +from .rust_marker import RustMarker +from .rust_chat_message import RustChatMessage +from .rust_team_info import RustTeamInfo, RustTeamMember, RustTeamNote +from .rust_entity_info import RustEntityInfo +from .rust_contents import RustContents +from .rust_item import RustItem +from .util import Vector diff --git a/rustplus/utils/__init__.py b/rustplus/utils/__init__.py index f0acb2b..7fcbb60 100644 --- a/rustplus/utils/__init__.py +++ b/rustplus/utils/__init__.py @@ -1,6 +1 @@ -from .rust_utils import * from .deprecated import deprecated -from .grab_items import translate_id_to_stack -from .server_id import ServerID -from .yielding_event import YieldingEvent -from .emojis import Emoji