def format_status(self, text): return text.format(self.isWorking(), self.isWorking() and not self.isAFKing(), utils.convert_millis(self.timeRecorded()), utils.convert_millis(self.timePassed()), self.packet_counter, utils.convert_file_size_MB(len(self.file_buffer)), utils.convert_file_size_MB(self.replay_file.size()), self.file_name)
def delete_marker(self, index): marker = self.replay_file.markers.pop(index - 1) self.chat( self.translation('OnMarkerDeleted').format( utils.convert_millis(marker['realTimestamp']))) self.logger.log( 'Marker deleted: {}, {} markers has been stored'.format( marker, len(self.replay_file.markers)))
def add_marker(self, name=None): if self.pos is None: self.logger.warn('Fail to add marker, position unknown!') return time_stamp = self.timeRecorded() marker = self.replay_file.add_marker(self.timeRecorded(), self.pos, name) self.chat( self.translation('OnMarkerAdded').format( utils.convert_millis(time_stamp))) self.logger.log('Marker added: {}, {} markers has been stored'.format( marker, len(self.replay_file.markers)))
def print_markers(self): if len(self.replay_file.markers) == 0: self.chat(self.translation('MarkerNotFound')) else: self.chat(self.translation('CommandMarkerListTitle')) for i in range(len(self.replay_file.markers)): name = self.replay_file.markers[i]['value'][ 'name'] if 'name' in self.replay_file.markers[i][ 'value'] else '' self.chat('{}. {} {}'.format( i + 1, utils.convert_millis( self.replay_file.markers[i]['realTimestamp']), name))
def __createReplayFile(self, logger): self.flush() if self.mc_version is None or self.mc_protocol is None: logger.log( 'Not connected to the server yet, abort creating replay recording file' ) return if self.replay_file.size() < utils.MinimumLegalFileSize: logger.warn( 'Size of "recording.tmcpr" too small ({}KB < {}KB), abort creating replay file' .format(utils.convert_file_size_KB(self.replay_file.size()), utils.convert_file_size_KB( utils.MinimumLegalFileSize))) return # Creating .mcpr zipfile based on timestamp logger.log('Time recorded/passed: {}/{}'.format( utils.convert_millis(self.timeRecorded()), utils.convert_millis(self.timePassed()))) # Deciding file name if not os.path.exists(utils.RecordingStorageFolder): os.makedirs(utils.RecordingStorageFolder) file_name_raw = datetime.datetime.today().strftime( 'PCRC_%Y_%m_%d_%H_%M_%S') if self.file_name is not None: file_name_raw = self.file_name file_name = file_name_raw + '.mcpr' counter = 2 while os.path.isfile(f'{utils.RecordingStorageFolder}{file_name}'): file_name = f'{file_name_raw}_{counter}.mcpr' counter += 1 logger.log('File name is set to "{}"'.format(file_name)) logger.log('Creating "{}"'.format(file_name)) if self.isOnline(): self.chat(self.translation('OnCreatingMCPRFile')) self.replay_file.meta_data = utils.get_meta_data( server_name=self.config.get('server_name'), duration=self.timeRecorded(), date=utils.getMilliTime(), mcversion=self.mc_version, protocol=self.mc_protocol, player_uuids=self.player_uuids) self.replay_file.create(file_name) logger.log('Size of replay file "{}": {}MB'.format( file_name, utils.convert_file_size_MB(os.path.getsize(file_name)))) file_path = f'{utils.RecordingStorageFolder}{file_name}' shutil.move(file_name, file_path) if self.isOnline(): self.chat(self.translation('OnCreatedMCPRFile').format(file_name), priority=ChatThread.Priority.High) if self.config.get('upload_file'): if self.isOnline(): self.chat(self.translation('OnUploadingMCPRFile')) logger.log('Uploading "{}" to transfer.sh'.format(file_name)) try: ret, out = subprocess.getstatusoutput( 'curl --upload-file {} https://transfer.sh/{}'.format( file_path, file_name)) url = out.splitlines()[-1] self.file_urls.append(url) if self.isOnline(): self.chat(self.translation('OnUploadedMCPRFile').format( file_name, url), priority=ChatThread.Priority.High) except Exception: logger.error( 'Fail to upload "{}" to transfer.sh'.format(file_name)) logger.error(traceback.format_exc())
def processPacketData(self, packet_raw): if not self.isWorking(): return bytes = packet_raw.data if bytes[0] == 0x00: bytes = bytes[1:] t = utils.getMilliTime() packet_length = len(bytes) packet = SARCPacket() packet.receive(bytes) packet_id, packet_name = self.packet_processor.analyze(packet) packet_recorded = self.packet_processor.process(packet) # Increase afk timer when recording stopped, afk timer prevents afk time in replays if self.config.get('with_player_only'): noPlayerMovement = self.noPlayerMovement(t) if noPlayerMovement: self.afk_time += t - self.last_t if self.last_no_player_movement != noPlayerMovement: msg = self.translation('RecordingPause') if self.isAFKing( ) else self.translation('RecordingContinue') self.chat(msg) self.last_no_player_movement = noPlayerMovement self.last_t = t # Recording if self.isWorking() and packet_recorded is not None: if not self.isAFKing( ) or packet_name in utils.IMPORTANT_PACKETS or self.config.get( 'record_packets_when_afk'): bytes_recorded = packet_recorded.read( packet_recorded.remaining()) data = self.timeRecorded().to_bytes(4, byteorder='big', signed=True) data += len(bytes_recorded).to_bytes(4, byteorder='big', signed=True) data += bytes_recorded self.write(data) self.packet_counter += 1 if self.isAFKing() and packet_name in utils.IMPORTANT_PACKETS: self.logger.debug( 'PCRC is afking but {} is an important packet so PCRC recorded it' .format(packet_name)) else: self.logger.debug('{} packet recorded'.format(packet_name)) else: self.logger.debug( '{} packet ignore due to being afk'.format(packet_name)) else: self.logger.debug('{} packet ignore'.format(packet_name)) pass if self.isWorking( ) and self.replay_file.size() > self.file_size_limit(): self.logger.log( 'tmcpr file size limit {}MB reached! Restarting'.format( utils.convert_file_size_MB(self.file_size_limit()))) self.chat( self.translation('OnReachFileSizeLimit').format( utils.convert_file_size_MB(self.file_size_limit()))) self.restart() if self.isWorking( ) and self.timeRecorded(t) > self.time_recorded_limit(): self.logger.log('{} actual recording time reached!'.format( utils.convert_millis(self.time_recorded_limit()))) self.chat( self.translation('OnReachTimeLimit').format( utils.convert_millis(self.time_recorded_limit()))) self.restart() def get_showinfo_time(): return int(self.timePassed(t) / (5 * 60 * 1000)) # Log information in console if get_showinfo_time( ) != self.last_showinfo_time or self.packet_counter - self.last_showinfo_packetcounter >= 100000: self.last_showinfo_time = get_showinfo_time() self.last_showinfo_packetcounter = self.packet_counter self.logger.log('Recorded/Passed: {}/{}; Packet count: {}'.format( utils.convert_millis(self.timeRecorded(t)), utils.convert_millis(self.timePassed(t)), self.packet_counter))
def run(config, email, password, debug, address): access_token, uuid, user_name = utils.get_token(email, password) clientbound, serverbound, protocol_version, mc_version = utils.generate_protocol_table(address) connection = utils.login(address, protocol_version, debug, access_token, uuid, user_name) print(clientbound) start_time = int(time.time() * 1000) last_player_movement = start_time entity_packets = ['Entity', 'Entity Relative Move', 'Entity Look And Relative Move', 'Entity Look', 'Entity Teleport'] player_uuids = [] player_ids = [] blocked_entity_ids = [] opped_players = [] write_buffer = bytearray() file_size = 0 afk_time = 0 last_t = 0 open('recording.tmcpr', 'w').close() # Cleans recording file time.sleep(0.5) utils.request_ops(connection, serverbound, protocol_version) # Request op list once if 'Time Update' in utils.BAD_PACKETS: utils.BAD_PACKETS.remove('Time Update') # Main processing loop for incoming data. while True: ready_to_read = select.select([connection.socket], [], [], 0)[0] if ready_to_read: t = int(time.time() * 1000) try: packet_in = connection.receive_packet() except IOError: break packet_recorded = int(t - start_time - afk_time).to_bytes(4, byteorder='big', signed=True) packet_recorded += len(packet_in.received).to_bytes(4, byteorder='big', signed=True) packet_recorded += packet_in.received packet_id = packet_in.read_varint() packet_name = clientbound[str(packet_id)] if debug: print('P Packet ' + hex(packet_id) + ': ' + packet_name) # Answer keep aLive if packet_name == 'Keep Alive (clientbound)': packet_out = Packet() packet_out.write_varint(serverbound['Keep Alive (serverbound)']) if protocol_version > 338: # For some unnecessary reason keepalive changes to long after 1.12.1 id = packet_in.read_long() packet_out.write_long(id) else: id = packet_in.read_varint() packet_out.write_varint(id) connection.send_packet(packet_out) # Respawn when dead if packet_name == 'Update Health': health = packet_in.read_float() food = packet_in.read_varint() food_sat = packet_in.read_float() if health == 0.0: packet_out = Packet() packet_out.write_varint(serverbound['Client Status']) packet_out.write_varint(0) connection.send_packet(packet_out) # If configured set daytime once and ignore all further time updates if (24000 > config['daytime'] > 0 and packet_name == 'Time Update' and not utils.is_bad_packet(packet_name, config['minimal_packets'])): print('Set daytime to: ' + str(config['daytime'])) packet_daytime = Packet() packet_daytime.write_varint( int(list(clientbound.keys())[list(clientbound.values()).index('Time Update')])) world_age = packet_in.read_long() packet_daytime.write_long(world_age) packet_daytime.write_long( -config['daytime']) # If negative sun will stop moving at the Math.abs of the time packet_recorded = int(t - start_time).to_bytes(4, byteorder='big', signed=True) packet_recorded += len(packet_daytime.received).to_bytes(4, byteorder='big', signed=True) packet_recorded += packet_daytime.received write_buffer += packet_recorded utils.BAD_PACKETS.append('Time Update') # Ignore all further updates # Remove weather if configured if not config['weather'] and packet_name == 'Change Game State': reason = packet_in.read_ubyte() if reason == 1 or reason == 2: packet_recorded = '' # Teleport confirm if packet_name == 'Player Position And Look (clientbound)': x = packet_in.read_double() y = packet_in.read_double() z = packet_in.read_double() yaw = packet_in.read_float() pitch = packet_in.read_float() flag = packet_in.read_byte() teleport_id = packet_in.read_varint() packet_out = Packet() packet_out.write_varint(serverbound['Teleport Confirm']) packet_out.write_varint(teleport_id) connection.send_packet(packet_out) # Update player list for metadata and player tracking if packet_name == 'Spawn Player': entity_id = packet_in.read_varint() if entity_id not in player_ids: player_ids.append(entity_id) uuid = packet_in.read_uuid() if uuid not in player_uuids: player_uuids.append(uuid) last_player_movement = int(time.time() * 1000) # Keep track of spawned items and their ids if ((config['remove_items'] or config['remove_bats']) and (packet_name == 'Spawn Object' or packet_name == 'Spawn Mob')): entity_id = packet_in.read_varint() uuid = packet_in.read_uuid() type = packet_in.read_byte() if ((packet_name == 'Spawn Object' and type == 2 and protocol_version > 340) or (packet_name == 'Spawn Mob' and ((type == 65 and protocol_version <= 340) or (type == 3 and protocol_version > 340)))): blocked_entity_ids.append(entity_id) packet_recorded = '' # Remove item pickup animation packet if config['remove_items'] and packet_name == 'Collect Item': packet_recorded = '' # Detecting player activity to continue recording and remove items or bats if packet_name in entity_packets: entity_id = packet_in.read_varint() if config['recording'] and entity_id in player_ids: last_player_movement = t if entity_id in blocked_entity_ids: recorded_packet = '' # Record all "joining" or "leaving" tab updates to properly start recording players # In 1.14.4 Player List Item changes to Player Info if (protocol_version < 498 and packet_name == 'Player List Item') or (protocol_version >= 498 and packet_name == 'Player Info'): action = packet_in.read_varint() if config['recording'] and action == 0: # int(time.time() * 1000) - last_player_movement <= 5000 and write_buffer += packet_recorded player_number = packet_in.read_varint() uuid = packet_in.read_uuid() name = packet_in.read_utf() # Handle chat and process ingame commands if packet_name == 'Chat Message (clientbound)': try: # For whatever reason there sometimes exists an empty chat packet.. chat = packet_in.read_utf() chat = json.loads(chat) if chat['translate'] == 'chat.type.text': name = chat['with'][0]['hoverEvent']['value']['text'].split(':"')[1].split('"', 1)[0] uuid = chat['with'][0]['hoverEvent']['value']['text'].split(':"')[2].split('"', 1)[0] message = chat['with'][1] if message == '!updateops': utils.request_ops(connection, serverbound, protocol_version) utils.send_chat_message(connection, serverbound, 'Updating OP list') if name in opped_players: print('<' + name + '(OP)> ' + message) else: print('<' + name + '> ' + message) if (config['require_op'] and name in opped_players) or not config['require_op']: if message == '!relog': should_restart = True print('Relogging...') break if message == '!stop': should_restart = False print('Stopping...') break if message == '!ping': utils.send_chat_message(connection, serverbound, 'pong!') if message == '!filesize': utils.send_chat_message(connection, serverbound, str(round(file_size / 1000000, 1)) + 'MB') if message == '!time': utils.send_chat_message(connection, serverbound, 'Recorded time: ' + utils.convert_millis( t - start_time - afk_time)) if message == '!timeonline': utils.send_chat_message(connection, serverbound, 'Time client was online: ' + utils.convert_millis( t - start_time)) if message == '!move': packet_out = Packet() packet_out.write_varint(serverbound['Spectate']) packet_out.write_uuid(uuid) connection.send_packet(packet_out) utils.send_chat_message(connection, serverbound, 'moved to ' + name) if message == '!glow': # Chat system got updated in 1.13 if protocol_version > 340: utils.send_chat_message(connection, serverbound, '/effect give @s minecraft:glowing 1000000 0 true') else: utils.send_chat_message(connection, serverbound, '/effect @p minecraft:glowing 1000000 0 true') except: pass # Process the requested list of opped players if packet_name == 'Tab-Complete (clientbound)': matches = [] if protocol_version <= 404: count = packet_in.read_varint() for i in range(count): matches.append(packet_in.read_utf()) else: id = packet_in.read_varint() start = packet_in.read_varint() length = packet_in.read_varint() count = packet_in.read_varint() for i in range(count): matches.append(packet_in.read_utf()) has_tooltip = packet_in.read_bool() if has_tooltip: tooltip = packet_in.read_utf() for match in matches: if match not in opped_players: opped_players.append(match) # Actual recording if (config['recording'] and t - last_player_movement <= 5000 and not utils.is_bad_packet(packet_name, config['minimal_packets'])): # To prevent constant writing to the disk a buffer of 8kb is used if packet_recorded != '': write_buffer += packet_recorded if len(write_buffer) > 8192: with open('recording.tmcpr', 'ab+') as replay_recording: replay_recording.write(write_buffer) if debug: print('Recorded:' + str(write_buffer)[:80] + '...') file_size += len(write_buffer) write_buffer = bytearray() # Prevent any kind of size increase due to packets beeing added to the buffer it gets cleared when not needed if not config['recording'] and len(write_buffer) > 0: write_buffer = bytearray() # Increase afk timer when recording stopped, afk timer prevents afk time in replays if config['recording'] and t - last_player_movement > 5000: afk_time += t - last_t last_t = t # Save last packet timestamp for afk delta # Every 150mb the client restarts if config['recording'] and file_size > 150000000: print('Filesize limit reached!') utils.send_chat_message(connection, serverbound, 'Filesize limit reached, restarting...') should_restart = True time.sleep(1) break # Every 5h recording the the client restarts elif config['recording'] and t - start_time - afk_time > 1000 * 60 * 60 * 5: print('5h recording reached!') utils.send_chat_message(connection, serverbound, '5h recording reached, restarting...') should_restart = True time.sleep(1) break else: time.sleep(0.0001) # Release to prevent 100% cpu usage # Handling the disconnect print('Disconnected') if config['recording'] and len(write_buffer) > 0: # Finish writing if buffer not empty with open('recording.tmcpr', 'ab+') as replay_recording: replay_recording.write(write_buffer) write_buffer = bytearray() print('Time client was online: ' + utils.convert_millis(t - start_time)) if config['recording']: print('Recorded time: ' + utils.convert_millis(t - start_time - afk_time)) # Create metadata file with open('metaData.json', 'w') as json_file: meta_data = {'singleplayer': 'false', 'serverName': address[0], 'duration': int(time.time() * 1000) - start_time - afk_time, 'date': int(time.time() * 1000), 'mcversion': mc_version, 'fileFormat': 'MCPR', 'generator': 'SARC', 'fileFormatVersion': utils.get_metadata_file_format(protocol_version), 'protocol': protocol_version, 'selfId': -1, 'players': player_uuids} json.dump(meta_data, json_file) # Creating .mcpr zipfile based on timestamp print('Creating .mcpr file...') date = datetime.datetime.today().strftime('SARC_%Y%m%d_%H_%S') zipf = zipfile.ZipFile(date + '.mcpr', 'w', zipfile.ZIP_DEFLATED) zipf.write('metaData.json') zipf.write('recording.tmcpr') os.remove('metaData.json') os.remove('recording.tmcpr') print('Finished!') connection.close() return should_restart