11import operator
22import random
33from collections import defaultdict
4- from time import time
4+ from threading import Lock
5+ from time import time , sleep
56
67from sqlalchemy import Table , Column , String , Integer , PrimaryKeyConstraint , desc , Boolean
78from sqlalchemy .sql import select
6566MSG_DELAY = 10
6667MASK_REQ = 3
6768scripters = defaultdict (int )
69+ chan_locks = defaultdict (lambda : defaultdict (Lock ))
6870game_status = defaultdict (lambda : defaultdict (lambda : defaultdict (int )))
6971
7072
@@ -93,21 +95,46 @@ def load_status(db):
9395 set_ducktime (chan , net )
9496
9597
98+ def save_channel_state (db , network , chan , status = None ):
99+ if status is None :
100+ status = game_status [network ][chan .casefold ()]
101+
102+ active = bool (status ['game_on' ])
103+ duck_kick = bool (status ['no_duck_kick' ])
104+ res = db .execute (status_table .update ().where (status_table .c .network == network ).where (
105+ status_table .c .chan == chan ).values (
106+ active = active , duck_kick = duck_kick
107+ ))
108+ if not res .rowcount :
109+ db .execute (status_table .insert ().values (network = network , chan = chan , active = active , duck_kick = duck_kick ))
110+
111+ db .commit ()
112+
113+
96114@hook .on_unload
97- @hook .periodic (5 * 60 , initial_interval = 10 * 60 )
98- def save_status (db ):
115+ def save_on_exit (db ):
116+ return save_status (db , False )
117+
118+
119+ # @hook.periodic(8 * 3600, singlethread=True) # Run every 8 hours
120+ def save_status (db , _sleep = True ):
99121 for network in game_status :
100122 for chan , status in game_status [network ].items ():
101- active = bool (status ['game_on' ])
102- duck_kick = bool (status ['no_duck_kick' ])
103- res = db .execute (status_table .update ().where (status_table .c .network == network ).where (
104- status_table .c .chan == chan ).values (
105- active = active , duck_kick = duck_kick
106- ))
107- if not res .rowcount :
108- db .execute (status_table .insert ().values (network = network , chan = chan , active = active , duck_kick = duck_kick ))
123+ save_channel_state (db , network , chan , status )
124+
125+ if _sleep :
126+ sleep (5 )
127+
109128
110- db .commit ()
129+ def set_game_state (db , conn , chan , active = None , duck_kick = None ):
130+ status = game_status [conn .name ][chan ]
131+ if active is not None :
132+ status ['game_on' ] = int (active )
133+
134+ if duck_kick is not None :
135+ status ['no_duck_kick' ] = int (duck_kick )
136+
137+ save_channel_state (db , conn .name , chan , status )
111138
112139
113140@hook .event ([EventType .message , EventType .action ], singlethread = True )
@@ -123,7 +150,7 @@ def incrementMsgCounter(event, conn):
123150
124151
125152@hook .command ("starthunt" , autohelp = False , permissions = ["chanop" , "op" , "botcontrol" ])
126- def start_hunt (chan , message , conn ):
153+ def start_hunt (db , chan , message , conn ):
127154 """- This command starts a duckhunt in your channel, to stop the hunt use .stophunt"""
128155 global game_status
129156 if chan in opt_out :
@@ -134,7 +161,8 @@ def start_hunt(chan, message, conn):
134161 if check :
135162 return "there is already a game running in {}." .format (chan )
136163 else :
137- game_status [conn .name ][chan ]['game_on' ] = 1
164+ set_game_state (db , conn , chan , active = True )
165+
138166 set_ducktime (chan , conn .name )
139167 message (
140168 "Ducks have been spotted nearby. See how many you can shoot or save. use .bang to shoot or .befriend to save them. NOTE: Ducks now appear as a function of time and channel activity." ,
@@ -153,29 +181,29 @@ def set_ducktime(chan, conn):
153181
154182
155183@hook .command ("stophunt" , autohelp = False , permissions = ["chanop" , "op" , "botcontrol" ])
156- def stop_hunt (chan , conn ):
184+ def stop_hunt (db , chan , conn ):
157185 """- This command stops the duck hunt in your channel. Scores will be preserved"""
158186 global game_status
159187 if chan in opt_out :
160188 return
161189 if game_status [conn .name ][chan ]['game_on' ]:
162- game_status [ conn . name ][ chan ][ 'game_on' ] = 0
190+ set_game_state ( db , conn , chan , active = False )
163191 return "the game has been stopped."
164192 else :
165193 return "There is no game running in {}." .format (chan )
166194
167195
168196@hook .command ("duckkick" , permissions = ["chanop" , "op" , "botcontrol" ])
169- def no_duck_kick (text , chan , conn , notice_doc ):
197+ def no_duck_kick (db , text , chan , conn , notice_doc ):
170198 """<enable|disable> - If the bot has OP or half-op in the channel you can specify .duckkick enable|disable so that people are kicked for shooting or befriending a non-existent goose. Default is off."""
171199 global game_status
172200 if chan in opt_out :
173201 return
174202 if text .lower () == 'enable' :
175- game_status [ conn . name ][ chan ][ 'no_duck_kick' ] = 1
203+ set_game_state ( db , conn , chan , duck_kick = True )
176204 return "users will now be kicked for shooting or befriending non-existent ducks. The bot needs to have appropriate flags to be able to kick users for this to work."
177205 elif text .lower () == 'disable' :
178- game_status [ conn . name ][ chan ][ 'no_duck_kick' ] = 0
206+ set_game_state ( db , conn , chan , duck_kick = False )
179207 return "kicking for non-existent ducks has been disabled."
180208 else :
181209 notice_doc ()
@@ -286,7 +314,7 @@ def update_score(nick, chan, db, conn, shoot=0, friend=0):
286314 return {'shoot' : shoot , 'friend' : friend }
287315
288316
289- def attack (nick , chan , message , db , conn , notice , attack ):
317+ def attack (event , nick , chan , message , db , conn , notice , attack ):
290318 global game_status , scripters
291319 if chan in opt_out :
292320 return
@@ -361,22 +389,25 @@ def attack(nick, chan, message, db, conn, notice, attack):
361389 score = update_score (nick , chan , db , conn , ** args )[attack_type ]
362390 except Exception :
363391 status ['duck_status' ] = 1
392+ event .reply ("An unknown error has occurred." )
364393 raise
365394
366395 message (msg .format (nick , shoot - deploy , pluralize (score , "duck" ), chan ))
367396 set_ducktime (chan , conn .name )
368397
369398
370399@hook .command ("bang" , autohelp = False )
371- def bang (nick , chan , message , db , conn , notice ):
400+ def bang (nick , chan , message , db , conn , notice , event ):
372401 """- when there is a duck on the loose use this command to shoot it."""
373- return attack (nick , chan , message , db , conn , notice , "shoot" )
402+ with chan_locks [conn .name ][chan .casefold ()]:
403+ return attack (event , nick , chan , message , db , conn , notice , "shoot" )
374404
375405
376406@hook .command ("befriend" , autohelp = False )
377- def befriend (nick , chan , message , db , conn , notice ):
407+ def befriend (nick , chan , message , db , conn , notice , event ):
378408 """- when there is a duck on the loose use this command to befriend it before someone else shoots it."""
379- return attack (nick , chan , message , db , conn , notice , "befriend" )
409+ with chan_locks [conn .name ][chan .casefold ()]:
410+ return attack (event , nick , chan , message , db , conn , notice , "befriend" )
380411
381412
382413def smart_truncate (content , length = 320 , suffix = '...' ):
0 commit comments