From 99ffe7f9e9e8417b90173bf9b658ead0784647da Mon Sep 17 00:00:00 2001 From: jerick Date: Tue, 27 Jan 2026 08:26:48 -0500 Subject: [PATCH] Smart hit assignation and chain monitoring --- __pycache__/config.cpython-311.pyc | Bin 2154 -> 2254 bytes config.py | 3 + main.py | 8 +- routers/__pycache__/config.cpython-311.pyc | Bin 5279 -> 5368 bytes routers/config.py | 6 +- .../bot_assignment.cpython-311.pyc | Bin 16243 -> 23765 bytes .../__pycache__/server_state.cpython-311.pyc | Bin 9077 -> 9122 bytes services/__pycache__/torn_api.cpython-311.pyc | Bin 8329 -> 8389 bytes services/bot_assignment.py | 182 +++++++++++++++--- services/server_state.py | 3 + services/torn_api.py | 14 +- static/config.js | 3 +- templates/config.html | 11 ++ 13 files changed, 188 insertions(+), 42 deletions(-) diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc index 8c658f41a0289cbb2e2e6dd7dff37b43364e307c..6ecb7f417f258498069fa24c4890dae999c1749a 100644 GIT binary patch delta 176 zcmaDQa88hKIWI340}#x9RG!(yI+0I;@y$l{{frauNlJ>Oh%RGdU|0>r5D>+m!W7J) zDYp3y<8KzZTcXY$j-Gz;A)dajLGd9TL9W3b{yr`*S%Hc*1tz~_3sx&q1+rH%dk7HE=vFa delta 98 zcmX>n_)36pIWI340}yncE6;quGLcV$@xVs){fulWOu-DABAX?cezP!X@=rEo4^~{s t@EN4CNEk@`;;_lhPbtkwwJXvDav6cRn050ib|yyd3k_ajNuB*vsBB_?ZYs1}44ztcbpX{v2O3Q7-d4;E|dCdEsO zpa(-e*||wT@lef4v==XWDe6YxH6(WQ@7msyT=1h@Rhp+7An$f9F>$nk5l8A$=Bp{xHemfVp~cnv42^IiyCSy z209*5dhtH*#~C&PK8li9XM>Q!56plx{$ZOSqaweS{$1)r-k2-LRbTESH0RLQ6DPmsYGKZ4J>H!|(dAoTuP4dHD^AkO4nl@I8;W zjblxtNCJdZ5`f@Rb&`Tm740M@{9pA0;k+m`L^02#h8OY-`vVu#j-oAbWw~6joW6B# zm&jQO!Y_7wY1v%8x^U7e;iJIAOK}(nn{)V0;P_Az`fR?FI}04o?nG}zt8?#x@*$f~ X!_d3fu+7oB2j^P2d%TynCh4`mk+7M~ delta 538 zcmY*VOK1~O6n*#2Bs25UH=mCAH`WrYP3t1y#uQ@347jPEf*VnH1ClOOl0ik%g&%6R z_N;_}1@VKpQ8%s>sh^Dt1)=Cl*FqVQg5btCEunbfp2t1!p8M{3e`@z?_9H0;Q{k;& zY2US<*$>kqsc)+My@FTi<8&UgF?Y2~!FoZc!KYZYphFd==BQTnZ=U*eGbm}-aVNMOOp zV%8o(-M&8^8Urgd2YgBEu|;QlamIRDXcdPAyG$J~nt<+v33 z8{KfTg`lCmZRkPMc*iJc;;MgkB>9|g`lXBAs1xA3U!r69>#v@xP(U$D@R!DT{tM+| pzQr9T#?9O18|D7#&!qT#%qMC8r%F9$>fC`uRv2FI(4Bep-al8ifiD06 diff --git a/routers/config.py b/routers/config.py index a56382e..4172076 100644 --- a/routers/config.py +++ b/routers/config.py @@ -42,7 +42,8 @@ async def get_config(): "HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL, "REASSIGN_DELAY": config_module.REASSIGN_DELAY, "ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT, - "ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER + "ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER, + "CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD } else: with open(path, "r", encoding="utf-8") as f: @@ -81,7 +82,8 @@ async def update_config(req: ConfigUpdateRequest): "HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL, "REASSIGN_DELAY": config_module.REASSIGN_DELAY, "ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT, - "ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER + "ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER, + "CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD } } diff --git a/services/__pycache__/bot_assignment.cpython-311.pyc b/services/__pycache__/bot_assignment.cpython-311.pyc index c1dcd73b8a5b061ef94de16fd2ba11fc8aeaf548..61e484ff87568464910f42abe999ee86eae09fd3 100644 GIT binary patch delta 9430 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

bkTbP>O%Bp6t+5@uAj}PAXa5 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 R=Oe9oW+(m?C}M)f?%%asY2E+; diff --git a/services/__pycache__/server_state.cpython-311.pyc b/services/__pycache__/server_state.cpython-311.pyc index cd6fb3447b2ca10d81f4ccd1c3f3d2ffe925a256..c19bd1cf8ba722c4fc575653eb021e5b5189bce6 100644 GIT binary patch delta 618 zcmezBw#c1tIWI340}$jqF3$`T-pKcwnepA^U(D-eQW$C&;@Lq`AW$L*p+RI8=VSpv zF&3aS*W`;VnvC3&f3p~h2&WZgrsk#ORK}+zCYNO9=f!8HOx9+NWo(_?$(qgefsMh~ zyo2Qmhr;GBti4Q(T9cbNYz4G|#ue#<2m=sdIQcwBAfwu5Mb3I=ZBvkl8Hi8<5quy* z8Axcd6d|-(fJ7}PpXNzrwBBsU+sr7W1ro6Zsbhkwn|zGVpV4!(82>4dwGRa~*}(cO zCJPApGWu?g6FSeN0oJ7g(h9Qn7E4ZMaS7C-TP($Si3LSan?gkc8N)X(7yZG^Xgqm= zWC9ylr^#eCDNTrPyg-J4ePcD*UMiX~Y4dEUnapf{AmO0NR&pVXm6QABtZl&hz&-K03OYGpCR-E0td2O$gsDc~&0k1tBi%`ZzW@|(;i@6TAaIZVEU(NYhjOdLd5g9wn% zi|j#!3y5F_5iB6W6G+@*Ny$tuDGCJfqb4&e8Y&2}Dt}-A5(}Kc6gr``IYd#Pkx^rE ii;^B={N&9_Wd=EnjB+0su#+Fb;$M(u<0eNc%K!kStBy1P delta 596 zcmZ4F{?(0dIWI340}$+;QX@MFCV%GhXLQ@_ z%zp}GEuWAk8(6>jWLqI$Mz77?Lg$$@z`9gGT0z#{V#&!YE`eHfi={X(v7iWQQQ zW60(!qCc1!jV7O!Oke}+G@fiMr3vwkC&&=6Z!9P0NJTToZ$2S4lbOvIBpf(7T`q*N zZ1NsCYa6gWu+KoAy2X;6T3HNnHygy=fe3>@3OGyhr`!g1AZk8`$ zwA2MD69*AiAOhs`B0CV_3?i681Ph4p01~%YQZkcEiUL6V$jJtZh6?;mx->S8_2rZ*CP)VO8-0D)DAu zWSGuS!%zZJ4+ZgzFjficlevfuNZk@nE6PmGOUbE>PfJWL$;{7-&rF&8 zQc_<$5hz=103==jVFSYh1+7-sD=e~;)upOgG#QIbCohrGU`*S5R%$LIW9H^;nO%&G tg`3snq8S-ECO64z0?9SScb~$or9H8IvY+DHzMxF)~`MVE(`Wq|gbr$&m^<0QH Optional[int]: + """Fetch chain timeout from Torn API. Returns seconds remaining, or None if no chain or error.""" + if not STATE.friendly_faction_id: + return None + + try: + url = f"https://api.torn.com/v2/faction/{STATE.friendly_faction_id}/chain?key={TORN_API_KEY}" + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status != 200: + return None + data = await resp.json() + + # Get timeout value in seconds + chain_data = data.get("chain", {}) + timeout = chain_data.get("timeout", 0) + return timeout + except Exception as e: + print(f"Error fetching chain timer: {e}") + return None + async def start(self): """Start the bot assignment loop""" if self.running: @@ -119,10 +149,125 @@ class BotAssignmentManager: return eid return None + async def check_chain_timer(self): + """Check chain timer and update chain state""" + timeout = await self.fetch_chain_timer() + + if timeout is None: + # No chain data or error + if self.chain_active: + print("Chain ended or API error - resetting chain state") + self.chain_active = False + self.assigned_friendlies.clear() + self.current_group_index = 0 + self.chain_warning_sent = False + return + + self.chain_timeout = timeout + threshold_seconds = CHAIN_TIMER_THRESHOLD * 60 + + # Check if we should enter chain mode + if timeout > 0 and timeout <= threshold_seconds and not self.chain_active: + print(f"Chain timer at {timeout}s (threshold: {threshold_seconds}s) - entering chain mode") + self.chain_active = True + self.assigned_friendlies.clear() + self.current_group_index = 0 + self.chain_warning_sent = False + + # Check if chain expired + elif timeout > threshold_seconds and self.chain_active: + print(f"Chain timer above threshold ({timeout}s > {threshold_seconds}s) - exiting chain mode") + self.chain_active = False + self.assigned_friendlies.clear() + self.current_group_index = 0 + self.chain_warning_sent = False + + # Check if chain expired (timeout = 0) + elif timeout == 0 and self.chain_active: + print("Chain expired - resetting chain state") + self.chain_active = False + self.assigned_friendlies.clear() + self.current_group_index = 0 + self.chain_warning_sent = False + + # Send 30-second warning + if self.chain_active and timeout <= 30 and not self.chain_warning_sent: + await self.send_chain_expiration_warning() + self.chain_warning_sent = True + + async def send_chain_expiration_warning(self): + """Send @here alert that chain is about to expire""" + try: + channel = self.bot.get_channel(ALLOWED_CHANNEL_ID) + if channel and STATE.friendly_faction_id: + faction_link = f"https://www.torn.com/factions.php?step=your#/tab=chain" + message = f"@here **CHAIN EXPIRING IN 30 SECONDS!** Attack a faction member to keep it alive!\n{faction_link}" + await channel.send(message) + print("Sent chain expiration warning") + except Exception as e: + print(f"Error sending chain warning: {e}") + + async def assign_next_chain_hit(self): + """Assign next hit in round-robin fashion through groups""" + # Only assign if there's no active assignment waiting + if self.active_targets: + return # Wait for current assignment to complete + + # Get list of group IDs (sorted for consistency) + group_ids = sorted(STATE.groups.keys()) + if not group_ids: + return + + # Try to find a group with available friendly and enemy + attempts = 0 + while attempts < len(group_ids): + # Get current group + group_id = group_ids[self.current_group_index % len(group_ids)] + + async with STATE.lock: + friendly_ids = STATE.groups[group_id].get("friendly", []) + enemy_ids = STATE.groups[group_id].get("enemy", []) + + # Find a friendly who hasn't been assigned yet + available_friendly = None + for fid in friendly_ids: + if fid not in self.assigned_friendlies: + available_friendly = fid + break + + # Find an attackable enemy + enemy_id = self.get_next_enemy_in_group(group_id, enemy_ids) + + if available_friendly and enemy_id: + # Make assignment + self.assigned_friendlies.add(available_friendly) + await self.assign_target(group_id, available_friendly, enemy_id) + print(f"Chain hit assigned: Group {group_id}, Friendly {available_friendly} -> Enemy {enemy_id}") + + # Move to next group for next assignment + self.current_group_index += 1 + return + + # Move to next group and try again + self.current_group_index += 1 + attempts += 1 + + # If we've tried all groups and no assignments possible + # Check if we need to reset assigned_friendlies (everyone has been assigned) + async with STATE.lock: + all_friendlies = set() + for assignments in STATE.groups.values(): + all_friendlies.update(assignments.get("friendly", [])) + + if self.assigned_friendlies and self.assigned_friendlies >= all_friendlies: + print("All friendlies have been assigned - resetting for round-robin") + self.assigned_friendlies.clear() + self.current_group_index = 0 + async def assignment_loop(self): - """Main loop that assigns targets and monitors status""" + """Main loop that monitors chain timer and assigns targets""" await self.bot.wait_until_ready() - print("Bot is ready, assignment loop running") + print("Bot is ready, assignment loop running with chain timer monitoring") first_run = True while self.running: @@ -136,31 +281,18 @@ class BotAssignmentManager: continue if first_run: - print("Bot activated - processing assignments") + print("Bot activated - chain timer monitoring enabled") first_run = False - # Process each group - has_assignments = False - async with STATE.lock: - for group_id, assignments in STATE.groups.items(): - friendly_ids = assignments.get("friendly", []) - enemy_ids = assignments.get("enemy", []) + # Check chain timer every 30 seconds + now = datetime.now() + if (now - self.last_chain_check).total_seconds() >= 30: + await self.check_chain_timer() + self.last_chain_check = now - if friendly_ids and enemy_ids: - has_assignments = True - - if not friendly_ids or not enemy_ids: - continue - - # Try to assign any unassigned enemies - enemy_id = self.get_next_enemy_in_group(group_id, enemy_ids) - if enemy_id: - friendly_id = self.get_next_friendly_in_group(group_id, friendly_ids) - if friendly_id: - await self.assign_target(group_id, friendly_id, enemy_id) - - if not has_assignments and STATE.bot_running: - print("No group assignments found - drag members into groups first!") + # If chain is active, try to assign next hit + if self.chain_active: + await self.assign_next_chain_hit() # Monitor active targets for status changes or timeouts await self.monitor_active_targets() diff --git a/services/server_state.py b/services/server_state.py index 5ae1597..e9075f2 100644 --- a/services/server_state.py +++ b/services/server_state.py @@ -28,6 +28,9 @@ class ServerState: # bot running flag self.bot_running: bool = False + # faction IDs for API monitoring + self.friendly_faction_id: Optional[int] = None + # concurrency lock for async safety self.lock = asyncio.Lock() diff --git a/services/torn_api.py b/services/torn_api.py index 83d9358..31e8606 100644 --- a/services/torn_api.py +++ b/services/torn_api.py @@ -5,9 +5,8 @@ from config import TORN_API_KEY from .ffscouter import fetch_batch_stats from .server_state import STATE -# ----------------------------- + # Tasks -# ----------------------------- friendly_status_task = None enemy_status_task = None @@ -15,9 +14,8 @@ enemy_status_task = None friendly_lock = asyncio.Lock() enemy_lock = asyncio.Lock() -# ----------------------------- + # Populate faction (memory only) -# ----------------------------- async def populate_faction(faction_id: int, kind: str): """ Fetch members + FFScouter estimates once and store in STATE. @@ -64,9 +62,8 @@ async def populate_faction(faction_id: int, kind: str): return True -# ----------------------------- + # Status refresh loop -# ----------------------------- async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, interval: int): """ Periodically refresh member statuses in STATE. @@ -95,10 +92,11 @@ async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, in await asyncio.sleep(interval) -# ----------------------------- + # Public API helpers -# ----------------------------- async def populate_friendly(faction_id: int): + # Store friendly faction ID for chain monitoring + STATE.friendly_faction_id = faction_id return await populate_faction(faction_id, "friendly") async def populate_enemy(faction_id: int): diff --git a/static/config.js b/static/config.js index 89180e8..d0301f9 100644 --- a/static/config.js +++ b/static/config.js @@ -8,7 +8,8 @@ const CONFIG_FIELDS = { "HIT_CHECK_INTERVAL": "hit-check-interval", "REASSIGN_DELAY": "reassign-delay", "ASSIGNMENT_TIMEOUT": "assignment-timeout", - "ASSIGNMENT_REMINDER": "assignment-reminder" + "ASSIGNMENT_REMINDER": "assignment-reminder", + "CHAIN_TIMER_THRESHOLD": "chain-timer-threshold" }; let sensitiveFields = []; diff --git a/templates/config.html b/templates/config.html index bfb0434..a98a30c 100644 --- a/templates/config.html +++ b/templates/config.html @@ -91,6 +91,17 @@ + + +

+

Chain Timer Settings

+
+ +

Start assigning hits when chain timer is at or below this many minutes

+ + +
+