33from PyQt5 .QtCore import QTimer , QCoreApplication , Qt , QRect , QUrl
44from PyQt5 .QtGui import QPixmap , QDesktopServices
55import msgspec
6+ from textdistance import length
67# from PyQt5.QtWidgets import QLineEdit, QInputDialog, QShortcut
78# from PyQt5.QtWidgets import QApplication, QFileDialog, QWidget
89import gameDefinedParameter
910from plugin_sdk .server_bridge import GameServerBridge
10- from shared_types .events import GameEndEvent , BoardUpdateEvent , GameStatusChangeEvent
11+ from shared_types .events import GameFinishedEvent , BoardUpdateEvent , GameStatusChangeEvent
1112import superGUI
1213import gameAbout
1314import gameSettings
2930import hashlib
3031import uuid
3132from Crypto .Cipher import AES
32- from Crypto .Util . Padding import pad , unpad
33+ from Crypto .Random import get_random_bytes
3334# from PyQt5.QtWidgets import QApplication
3435from country_name import country_name
3536import metaminesweeper_checksum
@@ -222,11 +223,13 @@ def game_state(self):
222223 @game_state .setter
223224 def game_state (self , game_state : str ):
224225 # print(self._game_state, " -> " ,game_state)
226+ if game_state == self ._game_state :
227+ return
225228 last_state = self ._game_state
226229
227- match self . _game_state :
230+ match last_state :
228231 case "playing" :
229- self .onGameEnd (game_state )
232+ self .onGameFinished (game_state )
230233 if game_state not in ("playing" , "show" , "joking" ):
231234 self .timer_10ms .stop ()
232235 self .unlimit_cursor ()
@@ -274,12 +277,11 @@ def game_state(self, game_state: str):
274277 "display" : 7 ,
275278 "showdisplay" : 8 ,
276279 }
277- if last_state != game_state :
278- event = GameStatusChangeEvent (
279- last_status = state_map .get (last_state , 0 ),
280- current_status = state_map .get (game_state , 0 ),
281- )
282- GameServerBridge .instance ().send_event (event )
280+ event = GameStatusChangeEvent (
281+ last_status = state_map .get (last_state , 0 ),
282+ current_status = state_map .get (game_state , 0 ),
283+ )
284+ GameServerBridge .instance ().send_event (event )
283285 self ._send_board_update_event ()
284286
285287
@@ -319,12 +321,14 @@ def minenum(self, minenum):
319321
320322 # 生命周期函数,正式的游戏结束时调用。由游戏状态的变更触发,当且仅当由playing变为其他状态
321323 # 处理数据相关。不处理前端显示
322- def onGameEnd (self , new_game_state ):
324+ def onGameFinished (self , new_game_state ):
323325 # 不论如何都必然生成数据
326+ if self .label .ms_board .game_board_state == utils .GameBoardState .Playing .value :
327+ self .label .ms_board .step_game_state ("replay" )
324328 self .dump_evf_file_data ()
325329 # 发信号给插件,游戏结束了
326330 board = self .label .ms_board .board
327- event = GameEndEvent (
331+ event = GameFinishedEvent (
328332 game_state = ['ready' , 'study' , 'show' , 'playing' , 'joking' , 'fail' ,
329333 'win' , 'jofail' , 'jowin' , 'display' , 'showdisplay' ].index (new_game_state ),
330334 nf = self .label .ms_board .rce == 0 ,
@@ -362,22 +366,17 @@ def onGameEnd(self, new_game_state):
362366 board = board if isinstance (board , list ) else board .into_vec_vec (),
363367 raw_data = self .label .ms_board .raw_data
364368 )
365- GameServerBridge .instance ().send_event (event )
366369
367370 # 强制保存stats.dat文件
368371 record = utils .StatsRecord (
369372 game_state = event .game_state ,
370- nf = event .nf ,
371373 row = event .row ,
372374 column = event .column ,
373375 mine_num = event .mine_num ,
374- rtime = event . rtime ,
376+ rtime_ms = self . label . ms_board . rtime_ms ,
375377 left = event .left ,
376378 right = event .right ,
377379 double = event .double ,
378- level = event .level ,
379- cl = event .cl ,
380- ce = event .ce ,
381380 rce = event .rce ,
382381 lce = event .lce ,
383382 dce = event .dce ,
@@ -387,33 +386,30 @@ def onGameEnd(self, new_game_state):
387386 flag = event .flag ,
388387 path = event .path ,
389388 start_time = event .start_time ,
390- end_time = event .end_time ,
391389 mode = event .mode ,
392- software = event .software ,
393- player_identifier = event .player_identifier ,
394- race_identifier = event .race_identifier ,
395- uniqueness_identifier = event .uniqueness_identifier ,
396390 is_official = event .is_official ,
397391 is_fair = event .is_fair ,
398392 op = event .op ,
399393 isl = event .isl ,
400394 pluck = event .pluck ,
401- board = event .board
395+ board_bytes = utils .board_list_to_bytes (event .board ),
396+ short_md5 = hashlib .md5 (self .label .ms_board .raw_data ).digest ()[:8 ]
402397 )
403- binary_data = msgspec .msgpack .encode (record )
404- # 简单的AES加密
398+ GameServerBridge .instance ().send_event (event )
399+ binary_data = record .encode ()
400+ # 简单的AES-CTR加密(将nonce与密文一起写入以便解密)
405401 key = bytes ([2 ,135 ,180 ,102 ,125 ,204 ,245 ,102 ,253 ,59 ,217 ,7 ,114 ,61 ,231 ,62 ]) # 16字节的密钥
406- cipher = AES . new ( key , AES . MODE_ECB )
407- padded_data = pad ( binary_data , AES .block_size )
408- encrypted_data = cipher .encrypt (padded_data )
402+ nonce = get_random_bytes ( 8 )
403+ cipher = AES . new ( key , AES .MODE_CTR , nonce = nonce )
404+ encrypted_data = cipher .encrypt (binary_data )
409405 dat_file_path = self .setting_path / 'stats.dat'
410- # 把二进制加密数据 编码成 base64 字符串(bytes)
411- b64_data = base64 .b64encode (encrypted_data )
412- is_empty = not os .path .exists (dat_file_path ) or os .path .getsize (dat_file_path ) == 0
406+ # 将 nonce(8字节) + ciphertext 一并写入文件,前两字节为总长度
413407 with open (dat_file_path , 'ab' ) as f :
414- if not is_empty :
415- f .write (b"\n " )
416- f .write (b64_data )
408+ blob = nonce + encrypted_data
409+ encrypted_data_length = len (blob )
410+ len_bytes = encrypted_data_length .to_bytes (2 , byteorder = "big" , signed = False )
411+ f .write (len_bytes )
412+ f .write (blob )
417413
418414
419415 # 根据策略保存录像文件到磁盘
@@ -718,7 +714,7 @@ def gameRestart(self, e=None): # 画界面,但是不埋雷,改数据而不
718714 # self.label.paint_cursor = False
719715 # self.label.setMouseTracking(False) # 鼠标未按下时,组织移动事件回调
720716
721- # 游戏结束画残局,改状态
717+ # 游戏结束画残局,改状态。前端的游戏结束逻辑
722718 def gameFinished (self ):
723719 if self .label .ms_board .game_board_state == 3 and self .end_then_flag :
724720 self .label .ms_board .win_then_flag_all_mine ()
0 commit comments