import csv import sys from datetime import datetime, timedelta import glob import os from typing import * from copy import deepcopy import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt # from matplotlib.dates import DateFormatter from typing import Dict class ResourcesReport: class Record: def __init__(self, time, value): if isinstance(time, datetime): self.time = time if isinstance(time, str): self.time = datetime.strptime(time, "%Y-%m-%d %H:%M:%S.%f") self.value = float(value) CSV_HEADER = ["Time", "Value"] def __init__(self, config): self.records_to_add : Dict = {} self.config: Dict = config if self.config['resample_rate'] == 'H': self.config['timedelta'] = timedelta(hours=1) elif self.config['resample_rate'] == 'D': self.config['timedelta'] = timedelta(days=1) elif self.config['resample_rate'] == 'M': self.config['timedelta'] = timedelta(minutes=1) else: exit("Invalid sample rate") self.total_gem_expected_value = 0 self.gem_expected_csv_records = list() self.action_point_csv_records: List[ResourcesReport.Record] = list() self.oil_csv_records: List[ResourcesReport.Record] = list() self.gem_csv_records: List[ResourcesReport.Record] = list() self.coin_csv_records: List[ResourcesReport.Record] = list() self.cube_csv_records: List[ResourcesReport.Record] = list() self.pt_csv_records: List[ResourcesReport.Record] = list() self.yellow_coin_csv_records: List[ResourcesReport.Record] = list() self.purple_coin_csv_records: List[ResourcesReport.Record] = list() self.player_exp_csv_records: List[ResourcesReport.Record] = list() self.records_dict : Dict[str: List[ResourcesReport.Record]] = { 'oil' :self.oil_csv_records, 'gem' :self.gem_csv_records, 'gem_expected' :self.gem_expected_csv_records, 'coin' :self.coin_csv_records, 'cube' :self.cube_csv_records, 'pt' :self.pt_csv_records, 'yellow_coin' :self.yellow_coin_csv_records, 'purple_coin' :self.purple_coin_csv_records, 'player_exp' :self.player_exp_csv_records, 'action_point' :self.action_point_csv_records, } def process_lines(self, lines): lines = iter(lines) for line in lines: # oil if line.find("[OCR_OIL_LIMIT") != -1: continue if line.find("[OCR_OIL") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2] self.oil_csv_records.append(self.Record(time_str, value_str)) # gem if line.find("[SHOP_GEMS") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2] self.gem_csv_records.append(self.Record(time_str, value_str)) # coin if line.find("[OCR_COIN_LIMIT") != -1: continue if line.find("[OCR_COIN") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2] self.coin_csv_records.append(self.Record(time_str, value_str)) # cube if line.find("[BUILD_CUBE_COUNT") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2] self.cube_csv_records.append(self.Record(time_str, value_str)) # pt if line.find("[Event_PT_limit]") != -1: continue if line.find("[Event_PT]") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[1] self.pt_csv_records.append(self.Record(time_str, value_str)) # yellow_coin if line.find("[SHOP_YELLOW_COINS") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2] self.yellow_coin_csv_records.append(self.Record(time_str, value_str)) # purple_coin if line.find("[SHOP_PURPLE_COINS") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2] self.purple_coin_csv_records.append(self.Record(time_str, value_str)) # player_exp if line.find("[OCR_PLAYER_EXP") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2].split("/")[0] self.player_exp_csv_records.append(self.Record(time_str, value_str)) # action_points if line.find(" | INFO | Action points:") != -1: line_splited = line.split(" | ") time_str, value_str = line_splited[0], line_splited[2].rstrip().split(" ")[2] value_str = value_str.split("(")[1].split(")")[0] self.action_point_csv_records.append(self.Record(time_str, value_str)) if line.find("Choose urgent commission") != -1: line = next(lines,"EoL") if "gem" in line: if "Duration: 2:00:00" in line: self.total_gem_expected_value += 5.5 if "Duration: 4:00:00" in line: self.total_gem_expected_value += 12 if "Duration: 8:00:00" in line: self.total_gem_expected_value += 22.5 line_splited = line.split(" | ") time_str, value_str = line_splited[0], str(self.total_gem_expected_value) self.gem_expected_csv_records.append(self.Record(time_str, value_str)) def sort_records(self): for name,records in self.records_dict.items(): records.sort(key=lambda x: x.time) def resample(self, records, name): # 重要!!!我不知道怎么动起来的,搞不懂就别动!!! if self.config['resample_rate'] == 'D': start_time = datetime(records[0].time.year, records[0].time.month, records[0].time.day)+self.config['timedelta'] elif self.config['resample_rate'] == 'H': start_time = datetime(records[0].time.year, records[0].time.month, records[0].time.day, records[0].time.hour)+self.config['timedelta'] elif self.config['resample_rate'] == 'M': start_time = datetime(records[0].time.year, records[0].time.month, records[0].time.day, records[0].time.hour, records[0].time.minute)+self.config['timedelta'] _ = start_time last_record = records[0] records_copy = records.copy() for record in records_copy: if record.time > _ >= last_record.time: while True: if _ >= record.time: break if _ - self.config['timedelta']< last_record.time <= _: _record = self.Record(_, last_record.value) records.append(_record) last_record = record _ += self.config['timedelta'] _record = self.Record(_, record.value) records.remove(record) records.append(_record) _ += self.config['timedelta'] continue if record.time <= _: if record == records_copy[-1]: _record = self.Record(_, record.value) records.remove(record) records.append(_record) break last_record = record records.remove(record) continue if name == 'gem': records_copy = deepcopy(records) d = -records_copy[0].value records_copy[0] = self.Record(records_copy[0].time,0) l = len(records) for _ in range(1,l): record=records[_] last_record = records[_-1] delta_v = record.value - last_record.value if delta_v > 150 or delta_v < 0: d -= delta_v records_copy[_].value = records_copy[_].value +d self.records_to_add.update({'gem_modified' : records_copy}) if name == 'pt': new_records = [] new_count = 1 l = len(records) for _ in range(1,l): record=records[_] last_record = records[_-1] new_records.append(last_record) if record.value < last_record.value: self.records_to_add.update({f'pt_event_{new_count}': new_records}) new_count += 1 new_records = [] last_record = record new_records.append(last_record) if new_count > 1: self.records_to_add.update({f'pt_event_{new_count}' : new_records}) def pre_draw_reshape(self, records): inserted = 0 records_copy = records.copy() for _ in range(1,len(records_copy)): if records_copy[_].time - records_copy[_-1].time > self.config['timedelta']: time_delta = (records_copy[_].time - records_copy[_-1].time)//self.config['timedelta'] value_delta = (records_copy[_].value - records_copy[_-1].value)/time_delta for __ in range(1,time_delta): records.insert(_+inserted, self.Record(records_copy[_-1].time + __*self.config['timedelta'] , records_copy[_-1].value + __*value_delta)) inserted += 1 def draw_plt(self, records, name): png_dir = f"./report/{self.config['config']}/PNG" if not os.path.exists(png_dir): os.makedirs(png_dir) if "gem" in name: if name == "gem": pass if "expected" in name: print(f"Post process - Plot Drawing Gem_get") df1 = pd.DataFrame({ "time": [record.time for record in records], "Expected": [record.value for record in records] }) df2 = pd.DataFrame({ "time": [record.time for record in self.records_dict['gem_modified']], "Real": [record.value for record in self.records_dict['gem_modified']] }) df1.set_index("time", inplace=True) df2.set_index("time", inplace=True) df_combined = pd.merge( df1, df2, on="time", how="outer" ).sort_values("time") df_combined = df_combined.fillna(method="ffill") df_combined = df_combined.fillna(method="bfill") width_needed = len(records) * 0.0125 if self.config['config'] in ["H", "M"] else len(records) * 0.3 width_max = 2 ^ 32 if width_needed > width_max: width = width_max else: width = width_needed width = max(10, width) height = width / 2 plt.figure(figsize=(width, height)) plt.plot(df_combined.index, df_combined["Real"], label="Real") plt.plot(df_combined.index, df_combined["Expected"], label="Expected") plt.legend() plt.xlabel("Time") plt.ylabel("Value") plt.title(f"{self.config['config']} - Gem_get Over Time") plt.xticks(rotation=45) plt.tight_layout() plt.savefig(f"{png_dir}/Gem_get.png", dpi=150, bbox_inches="tight") plt.close() return if "modified" in name: return print(f"Post process - Plot Drawing {name.capitalize()}") df = pd.DataFrame({ "time": [record.time for record in records], name.capitalize(): [record.value for record in records] }) df.set_index("time", inplace=True) width_needed = len(records) * 0.0125 if self.config['config'] in ["H","M"] else len(records) * 0.3 width_max = 2^32 if width_needed > width_max: width = width_max else: width = width_needed width = max(10, width) height = width/2 ax = df.plot( kind="line", style="-", figsize=(width, height), title=f"{self.config['config']} - {name.capitalize()} Over Time", grid=True, ) ax.set_xlabel("time") ax.set_ylabel(name.capitalize()) plt.xticks(rotation=45) plt.tight_layout() plt.savefig(f"{png_dir}/{name.capitalize()}.png", dpi=150, bbox_inches="tight") plt.close() def post_process(self): print(f"Post process - Sorting") self.sort_records() for name, records in self.records_dict.items(): if not records: continue print(f"Post process - Resampling {name.capitalize()}") self.resample(records, name) if self.config['resample_rate'] == "M": if len(records) >= 24 * 60: # and name in [ # 'oil', # 'coin', # 'gem', # 'gem_modified', # ]: self.config['resample_rate'] = "H" self.config['timedelta'] = timedelta(hours=1) self.resample(records, name) print(f'WARNING - {name.capitalize()} has too many records, forced resample to hourly statistics.') self.config['resample_rate'] = "M" self.config['timedelta'] = timedelta(minutes=1) if self.config['resample_rate'] == "H": if len(records)>=60*24: # and name in [ # 'oil', # 'coin', # 'gem', # 'gem_modified', # ]: self.config['resample_rate'] = "D" self.config['timedelta'] = timedelta(days=1) self.resample(records, name) print(f'WARNING - {name.capitalize()} has too many records, forced resample to daily statistics.') self.config['resample_rate'] = "H" self.config['timedelta'] = timedelta(hours=1) self.records_dict.update(self.records_to_add) # if 'pt_event_1' in self.records_dict.keys(): # self.records_dict.pop('pt') for name, records in self.records_dict.items(): if not records: continue self.draw_plt(records, name) def dump_single(self, f, l): with open(f, mode="w", encoding="utf-8", newline="") as fd: writer = csv.writer(fd) writer.writerow(self.CSV_HEADER) writer.writerows([ [i.time.strftime("%Y-%m-%d %H:%M:%S.%f"), str(i.value)] for i in l ]) def dump_to_file(self): config_report_dir = f"./report/{self.config['config']}" if not os.path.exists(config_report_dir): os.makedirs(config_report_dir) print(f"Finishing - Generate report to {config_report_dir}") csv_dir = f"./report/{self.config['config']}/CSV" if not os.path.exists(csv_dir): os.makedirs(csv_dir) for name, records in self.records_dict.items(): self.dump_single(f"{csv_dir}/{name.capitalize()}.csv", records) print(f"Finished - Report generated") def gen_res_report(self): l = glob.glob(f"./log/*_{self.config['config']}.txt") for log in l: print(f"Line process - {log} - {os.stat(log).st_size} Bytes") with open(log, mode="r", encoding="utf-8") as f: lines = f.readlines() self.process_lines(lines) self.post_process() self.dump_to_file() def main(): if len(sys.argv) < 2: print("Invalid args.") exit(-1) _ = 1 conf = { "config": "", "resample_rate": "" } while _ < len(sys.argv): arg = sys.argv[_] if arg in ["--config-name", "--config", "-c"]: if _+1 >= len(sys.argv): exit("Invalid args.") conf["config"] = sys.argv[_+1] _ += 2 continue if arg in ["--resample-rate","-r"]: if _+1 >= len(sys.argv): exit("Invalid resample rate.") conf["resample_rate"] = sys.argv[_+1] _ += 2 continue if arg.upper() in ["M","D","H","HOUR","MINUTE","DAY","24"] and len(sys.argv) >2 and conf["resample_rate"] == ""\ and _+1 < len(sys.argv) and sys.argv[_+1].upper() not in ["M","D","HOUR","MINUTE","DAY","24"]\ and sys.argv[_+1] not in ["--resample-rate","-r"]: conf["resample_rate"] = arg _ += 1 continue if arg.upper() in ["M","D","H","HOUR","MINUTE","DAY","24"] and conf["config"] != ""\ and _+1 == len(sys.argv) : conf["resample_rate"] = arg _ += 1 continue if conf['config'] == "": conf["config"] = arg _ += 1 else: exit("Invalid args.") if conf["resample_rate"].upper() in ["M",'MINUTE']: conf["resample_rate"] = 'M' elif conf["resample_rate"].upper() in ["H",'HOUR','']: conf["resample_rate"] = 'H' elif conf["resample_rate"] .upper() in ["D","DAY",'24']: conf["resample_rate"] = 'D' else: exit("Invalid resample rate.") rate ={ 'M' : 'Per Minute', 'D' : 'Per Day', 'H' : 'Per Hour', } print(f"Starting with config: \n" f"Config name : {conf['config']}\n" f"Resample rate: {rate[conf['resample_rate']]}") reporter = ResourcesReport(conf) reporter.gen_res_report() if __name__ == "__main__": main()