├── LICENSE ├── README.md └── main_xizo.py /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | phisap 4 |

5 | 6 | # Phigros-Script 7 | 8 | ✨ 一个可以自动循环打歌的Phigros脚本 ✨ 9 | 10 | 轻松获取超多Data~ 11 | 12 | 修改自[kvarenzn/phisap](https://github.com/kvarenzn/phisap) 13 | 14 |
15 | 16 | ## 目录 17 | * [免责声明](#免责声明) 18 | * [简介](#简介) 19 | * [效果展示](#效果展示) 20 | * [如何使用](#如何使用) 21 | * [准备](#准备) 22 | * [运行](#运行) 23 | * [注意事项](#注意事项) 24 | * [致谢](#致谢) 25 | * [开源许可](#开源许可) 26 | 27 | ## 免责声明 28 | + 本项目属于个人兴趣项目,与厦门鸽游网络有限公司无关。 29 | + **本项目内不含任何版权素材,且本项目并非商业项目**。 30 | + 截止目前,项目作者从未在任何除GitHub以外的平台上以任何方式宣传过本项目。 31 | 32 | ## 简介 33 | 34 | 如你所见,这是一个用来刷Data的脚本,只要点按钮,就可以一直循环打歌,简单的获取大量Data。 35 | 36 | + 由于之前换手机,当时没出云存档功能,也忘了备份存档,所以旧的存档彻底没了。后来一段时间没玩Phi。 37 | + 而再次解锁精选集歌曲所需的Data实在不是小数目,于是就来找脚本了,没找到能用的,就在Phisap的基础上进行了修改。 38 | 39 | + 此处感谢一下,Phisap项目:[kvarenzn/phisap](https://github.com/kvarenzn/phisap) 40 | 41 | 42 | --- 43 | 44 | 45 | ![image](https://github.com/Xizo-114514/Phigros-Script/assets/120782087/7a94f162-bfbf-4d3f-8c86-7a3f3e9c1329) 46 | B站UP总结的打Data最快的曲,链接:[论 Phigros 刷 Data 的最快方式](https://www.bilibili.com/read/cv15590536) 47 | 48 | + 目前Phigros有88首精选集,全部解锁需要87首16MB和1首打折4MB,总共1396MB。 49 | + 如果打BetterGraphicAnimation,需要把这个曲AP约1396次,最快也需要42.6小时。 50 | 51 | 所以写了这个程序,仅需要电脑后台挂着就能刷Data。 52 | + 选用了BetterGraphicAnimation和Engine x Start!!两个曲子 53 | + 一首是IN难度曲子中最快的,一首是更容易解锁的,没有适配更多,因为延迟不好调。 54 | 55 | ## 效果展示 56 | 57 | ![image](https://github.com/Xizo-114514/Phigros-Script/assets/120782087/4efe0cd8-8d69-4cbb-ae89-9f97fcd1e9fe) 58 | 59 | 打BetterGraphicAnimation速度大约11.67KB/s ,已经最快了 60 | 61 | ## 如何使用 62 | 63 | ### 准备 64 | 65 | + 1.首先你要先去配置好Phisap,链接:[kvarenzn/phisap](https://github.com/kvarenzn/phisap) 66 | + 在你已经顺利地使用Phisap完成一次自动打歌之后,你才可以使用本项目的程序。 67 | 68 | + 2.确认Phisap可以正常工作,然后下载本项目。 69 | 70 | + 3.直接把main_xizo.py放到Phisap目录中。与main.py同一目录即可。 71 | + 说明:我的main_xizo.py是由Phisap中的main.py修改与精简而来的,必须放在目录中运行。 72 | 73 | ### 运行 74 | 75 | ```bash 76 | cd phisap # 将当前工作目录设置为phisap的根目录,或者直接在目录中右键选择“在终端中打开”(Win11) 77 | python main_xizo.py 78 | ``` 79 | + 程序内有提示,照做就可 80 | 81 | ## 注意事项 82 | 83 | 我的版本: Phigros 3.1.1.1 、 Python 3.11.4 、 Windows 11 、 BlueStacks 5.12.110.1006 P64模拟器 分辨率 960 x 536 84 | 85 | + 1.可能延迟对不上?可以尝试用同一个版本的Phigros。或者那就自己改改代码吧,就是245/249/251行的那几个值。 86 | 87 | + 2.程序跑不起来?用同版本Python试试。或者自己Debug。 88 | 89 | + 3.请只连接一个设备,可重置adb再次连接。 90 | 91 | + 4.建议用模拟器,连接手机比较麻烦,直接模拟器和脚本挂后台更方便。因为模拟器端口可能频繁变化,所以做了快速连接。 92 | 93 | + 5.模拟器建议降低分辨率,我用的960 x 536,更稳定点。如果出现异常问题,可以试试改分辨率。 94 | 95 | + 6.不要用WSA!之前用其他scrcpy脚本,怎么弄都不行,WSA有问题不要用,想找支持Hyper-V的模拟器可以用蓝叠64位,打不开就管理员运行。(亲身试出来的) 96 | 97 | + 7.别的自己研究研究不行再说。 98 | 99 | ## 致谢 100 | 101 | [kvarenzn/phisap](https://github.com/kvarenzn/phisap) 102 | 103 | [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy) 104 | 105 | [Perfare/AssetStudio](https://github.com/Perfare/AssetStudio) 106 | 107 | 感谢上述优秀的项目和创造或维护它们的个人或企业。 108 | 109 | ## 开源许可 110 | 111 | 以WTFPL协议开源 112 | 113 | 好怪的协议名字...... 114 | -------------------------------------------------------------------------------- /main_xizo.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import ctypes 3 | import inspect 4 | import json 5 | import os 6 | import subprocess 7 | from threading import Thread 8 | from tkinter import ttk, messagebox, Tk, X, IntVar, StringVar 9 | from typing import Iterator 10 | 11 | from rich.console import Console 12 | 13 | from algo.algo_base import TouchEvent 14 | from algo.algo_base import load_from_json, export_to_json 15 | from chart import Chart 16 | from control import DeviceController 17 | 18 | 19 | def agreement(): 20 | if os.path.exists('./cache'): 21 | return 22 | if not messagebox.askyesno(title='用户协定', message='您因使用或修改本程序发生的一切后果将由您自己承担而与程序原作者无关。\n' '您是否同意?'): 23 | exit(1) 24 | 25 | class App(ttk.Frame): 26 | 27 | cache: configparser.ConfigParser | None 28 | serials: list[str] 29 | running: bool 30 | start_time: float 31 | controller: DeviceController | None 32 | player_worker_thread: Thread | None 33 | console: Console 34 | 35 | def __init__(self, master: Tk): 36 | super().__init__(master) 37 | self.console = Console() 38 | self.controller = None 39 | self.player_worker_thread = None 40 | self.cache_path = None 41 | self.running = True 42 | self.start_time = 0.0 43 | self.cache = None 44 | self.pack() 45 | 46 | ttk.Separator(orient='horizontal').pack(fill=X) 47 | 48 | frm = ttk.Frame() 49 | frm.pack() 50 | 51 | ttk.Label(frm, text='选择设备: ').grid(column=0, row=0) 52 | self.serial = StringVar() 53 | self.serial_select = ttk.Combobox(frm, width=18, state='readonly', values=[], textvariable=self.serial) 54 | self.serial_select.grid(column=1, row=0) 55 | self.serial_select.bind('<>', self.adb_serial_selected) 56 | self.serial_refresh_btn = ttk.Button(frm, width=14, text='刷新', command=self.detect_adb_devices) 57 | self.serial_refresh_btn.grid(column=2, row=0) 58 | 59 | ttk.Separator(orient='horizontal').pack(fill=X) 60 | 61 | frm = ttk.Frame() 62 | frm.pack() 63 | 64 | ttk.Label(frm, text='手动连接: 127.0.0.1:').grid(column=0, row=0) 65 | self.port_bar = ttk.Entry(frm, width=8) 66 | self.port_bar.grid(column=1, row=0) 67 | self.adb_connect_btn = ttk.Button(frm, width=7, text='连接', command=self.adb_connect_devices) 68 | self.adb_connect_btn.grid(column=2, row=0) 69 | self.adb_rest_btn = ttk.Button(frm, width=10, text='重置adb', command=self.adb_rest) 70 | self.adb_rest_btn.grid(column=3, row=0) 71 | 72 | ttk.Separator(orient='horizontal').pack(fill=X) 73 | 74 | self.song_select = IntVar() 75 | self.song_select.set(0) 76 | 77 | frm = ttk.Frame() 78 | frm.pack() 79 | 80 | ttk.Label(frm, text='曲目选择:').grid(column=0, row=0) 81 | 82 | self.song_select1 = ttk.Radiobutton( 83 | frm, text='Better Graphic Animation |刷Data最快| ', variable=self.song_select, value=0 84 | ) 85 | self.song_select2 = ttk.Radiobutton( 86 | frm, text='Engine x Start!! (melody mix) |更易解锁| ', variable=self.song_select, value=1 87 | ) 88 | self.song_select1.grid(column=0, row=1, sticky='W') 89 | self.song_select2.grid(column=0, row=2, sticky='W') 90 | 91 | ttk.Separator(orient='horizontal').pack(fill=X) 92 | 93 | self.xizobtn = ttk.Button(text='准备自动打歌', command=self.xizorun) 94 | self.xizobtn.pack(anchor='center', expand=1) 95 | 96 | ttk.Separator(orient='horizontal').pack(fill=X) 97 | 98 | self.info_label = ttk.Label() 99 | self.info_label.pack() 100 | 101 | ttk.Separator(orient='horizontal').pack(fill=X) 102 | 103 | self.round_log_label = ttk.Label() 104 | self.round_log_label.pack() 105 | 106 | ttk.Separator(orient='horizontal').pack(fill=X) 107 | 108 | self.log_label = ttk.Label() 109 | self.log_label.pack() 110 | 111 | ttk.Separator(orient='horizontal').pack(fill=X) 112 | 113 | self.log_label['text'] = '| 已运行:---- | ---- 次 | 预估Data:---- MB |' 114 | self.round_log_label['text'] = '| 本次用时:---- s | 平均速度:---- KB/s |' 115 | self.update() 116 | 117 | agreement() 118 | 119 | def adb_connect_devices(self): 120 | subprocess.run(['adb', 'connect', '127.0.0.1:'+self.port_bar.get()]) 121 | return self 122 | 123 | def adb_rest(self): 124 | subprocess.run(['adb', 'kill-server']) 125 | subprocess.run(['adb', 'start-server']) 126 | return self 127 | 128 | def detect_adb_devices(self): 129 | self.serial_select['values'] = DeviceController.get_devices() 130 | return self 131 | 132 | def adb_serial_selected(self, event): 133 | serial = event.widget.get() 134 | print(serial) 135 | 136 | def xizorun(self): 137 | try: 138 | import time 139 | 140 | self.times = 0 141 | self.lasttime = 0 142 | self.mainstarttime = time.time() 143 | 144 | def logall(): 145 | while True: 146 | nowtime = time.time() 147 | runtimes = nowtime - self.mainstarttime 148 | m, s = divmod(runtimes, 60) 149 | h, m = divmod(m, 60) 150 | rantime = "%02d:%02d:%02d" % (h, m, s) 151 | if self.song_select.get() == 0: 152 | datas = str(self.times * 1.25) 153 | else: 154 | datas = str(self.times) 155 | self.log_label['text'] = '| 已运行:'+rantime+' | '+str(self.times)+' 次 | 预估Data:'+datas+' MB |' 156 | self.update() 157 | time.sleep(1) 158 | 159 | self.logall = Thread(target=logall, daemon=True) 160 | self.logall.start() 161 | 162 | if self.controller is None: 163 | self.controller = DeviceController() 164 | print('[client]', '正在确认设备尺寸,请稍候') 165 | time.sleep(1) 166 | print('[client]', f'设备尺寸: {self.controller.device_width}x{self.controller.device_height}') 167 | 168 | device_width = self.controller.device_width 169 | device_height = self.controller.device_height 170 | 171 | height = device_height 172 | width = height * 16 // 9 173 | xoffset = (device_width - width) >> 1 174 | yoffset = (device_height - height) >> 1 175 | scale_factor = height / 720 176 | 177 | self.info_label['text'] = '准备就绪\nTip: 请开一首曲子,再暂停,然后再点击开始\n或者在歌曲结算界面点击开始' 178 | self.xizobtn['text'] = '开始' 179 | self.round_log_label['text'] = '| 本次用时:---- s | 平均速度:---- KB/s |' 180 | self.update() 181 | 182 | self.running = True 183 | 184 | def go_now(): 185 | def stop(): 186 | self.running = False 187 | 188 | self.running = True 189 | 190 | # BetterGraphicAnimation.ルゼ.0 191 | # EnginexStartmelodymix.CrossingSound.0 192 | if self.song_select.get() == 0: 193 | chart_path = f'./Assets/Tracks/BetterGraphicAnimation.ルゼ.0/Chart_IN.json' 194 | else: 195 | chart_path = f'./Assets/Tracks/EnginexStartmelodymix.CrossingSound.0/Chart_IN.json' 196 | 197 | ans: dict 198 | ans_file = chart_path + '.ans.json' 199 | try: 200 | ans = load_from_json(open(ans_file)) 201 | except: 202 | import algo.algo1 203 | chart = Chart.from_dict(json.load(open(chart_path))) 204 | ans = algo.algo1.solve(chart, self.console) 205 | export_to_json(ans, open(ans_file, 'w')) 206 | ans = load_from_json(open(ans_file)) 207 | 208 | adapted_ans = [ 209 | (timestamp, [ev.map_to(xoffset, yoffset, scale_factor, scale_factor) for ev in ans[timestamp]]) 210 | for timestamp in sorted(ans.keys()) 211 | ] 212 | 213 | ans_iter = iter(adapted_ans) 214 | 215 | self.player_worker_thread = Thread(target=player_worker, args=(ans_iter,), daemon=True) 216 | 217 | def gogogo(): 218 | def stop(): 219 | def stop_thread(t): 220 | def _async_raise(tid, exctype): 221 | """raises the exception, performs cleanup if needed""" 222 | tid = ctypes.c_long(tid) 223 | if not inspect.isclass(exctype): 224 | exctype = type(exctype) 225 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)) 226 | if res == 0: 227 | raise ValueError("invalid thread id") 228 | elif res != 1: 229 | # """if it returns a number greater than one, you're in trouble, 230 | # and you should call it again with exc=NULL to revert the effect""" 231 | ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) 232 | raise SystemError("PyThreadState_SetAsyncExc failed") 233 | _async_raise(t.ident, SystemExit) 234 | stop_thread(self.go_thread) 235 | self.xizobtn['command'] = go_now 236 | self.xizobtn['text'] = '开始' 237 | self.info_label['text'] = '准备就绪\nTip: 请开一首曲子,再暂停,然后再点击开始\n或者在歌曲结算界面点击开始' 238 | self.running = False 239 | 240 | self.xizobtn['command'] = stop 241 | self.info_label['text'] = '正在等待第一个Note出现' 242 | self.update() 243 | 244 | self.controller.tap(30, 30) 245 | time.sleep(0.83) 246 | self.controller.tap(device_width >> 1, device_height >> 1) # (480, 268) 247 | 248 | if self.song_select.get() == 0: 249 | time.sleep(13.16) # Better Graphic Animation 250 | else: 251 | time.sleep(3.3) # Engine x Start!! 252 | 253 | self.player_worker_thread.start() 254 | 255 | #print("Started!") 256 | 257 | def stop(): 258 | self.running = False 259 | self.xizobtn['command'] = stop 260 | self.info_label['text'] = '正在打歌中......' 261 | self.update() 262 | 263 | self.go_thread = Thread(target=gogogo, daemon=True) 264 | self.go_thread.start() 265 | self.xizobtn['command'] = stop 266 | self.xizobtn['text'] = '停止' 267 | 268 | self.update() 269 | 270 | def player_worker(ans_iter: Iterator[tuple[int, list[TouchEvent]]]) -> None: 271 | """打歌线程""" 272 | if self.controller: 273 | timestamp, events = next(ans_iter) 274 | self.start_time = time.time() - timestamp / 1000 - 0.01 # 0.01 for the delay time 275 | 276 | try: 277 | while self.running: 278 | now = round((time.time() - self.start_time) * 1000) 279 | if now >= timestamp: 280 | for event in events: 281 | self.controller.touch(*event.pos, event.action, pointer_id=event.pointer) 282 | timestamp, events = next(ans_iter) 283 | except StopIteration: 284 | pass 285 | else: 286 | self.console.print('self.controller == None') 287 | 288 | if self.running == True: 289 | self.info_label['text'] = '即将自动开始循环' 290 | self.update() 291 | 292 | def rego(): 293 | if self.song_select.get() == 0: 294 | time.sleep(7.5) # Better Graphic Animation 295 | else: 296 | time.sleep(6) # Engine x Start!! 297 | go_now() 298 | 299 | self.re_go_thread = Thread(target=rego, daemon=True) 300 | self.re_go_thread.start() 301 | 302 | def stop(): 303 | def stop_thread(t): 304 | def _async_raise(tid, exctype): 305 | """raises the exception, performs cleanup if needed""" 306 | tid = ctypes.c_long(tid) 307 | if not inspect.isclass(exctype): 308 | exctype = type(exctype) 309 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)) 310 | if res == 0: 311 | raise ValueError("invalid thread id") 312 | elif res != 1: 313 | # """if it returns a number greater than one, you're in trouble, 314 | # and you should call it again with exc=NULL to revert the effect""" 315 | ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) 316 | raise SystemError("PyThreadState_SetAsyncExc failed") 317 | _async_raise(t.ident, SystemExit) 318 | stop_thread(self.re_go_thread) 319 | self.xizobtn['command'] = go_now 320 | self.xizobtn['text'] = '开始' 321 | self.info_label['text'] = '准备就绪\nTip: 请开一首曲子,再暂停,然后再点击开始\n或者在歌曲结算界面点击开始' 322 | self.running = False 323 | 324 | self.xizobtn['command'] = stop 325 | self.update() 326 | endtime = time.time() 327 | self.times = self.times + 1 328 | 329 | if self.lasttime != 0: 330 | parttime = endtime - self.lasttime 331 | if self.song_select.get() == 0: 332 | datakbps = str('%.2f' % (1280 / parttime)) 333 | else: 334 | datakbps = str('%.2f' % (1024 / parttime)) 335 | usedtime = str('%.1f' % parttime) 336 | self.lasttime = endtime 337 | self.round_log_label['text'] = '| 本次用时:'+usedtime+' s | 平均速度:'+datakbps+' KB/s |' 338 | self.update() 339 | else: 340 | self.lasttime = endtime 341 | self.round_log_label['text'] = '| 本次用时:---- s | 平均速度:---- KB/s |' 342 | self.update() 343 | 344 | else: 345 | self.xizobtn['command'] = go_now 346 | self.xizobtn['text'] = '开始' 347 | self.info_label['text'] = '准备就绪\nTip: 请开一首曲子,再暂停,然后再点击开始\n或者在歌曲结算界面点击开始' 348 | self.update() 349 | 350 | self.xizobtn['command'] = go_now 351 | self.update() 352 | except Exception: 353 | self.console.print_exception(show_locals=True) 354 | 355 | if __name__ == '__main__': 356 | tk = Tk() 357 | tk.title('Phisap - Revision by Xizo') 358 | # tk.iconbitmap('icon.ico') 359 | App(tk).detect_adb_devices().mainloop() 360 | --------------------------------------------------------------------------------