fix que / add auto
This commit is contained in:
196
routes/api.py
196
routes/api.py
@@ -146,10 +146,103 @@ def receive_state():
|
||||
''', (str(player_id), world_id, town_id_cel, t_name, cel_type, finished_at, now_iso))
|
||||
conn.commit()
|
||||
|
||||
# ── Auto-culture: check per-town settings and queue if eligible ──
|
||||
# Runs on every state push — no extra game polling needed.
|
||||
_auto_culture_check(c, str(player_id), world_id, towns, battle_points)
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
return jsonify({'ok': True, 'towns_updated': len(towns)})
|
||||
|
||||
|
||||
# Cost constants (must match dashboard.py PARTY_COST / TRIUMPH_COST)
|
||||
_PARTY_COST = {'wood': 15000, 'stone': 18000, 'iron': 15000}
|
||||
_TRIUMPH_BP = 300
|
||||
|
||||
def _auto_culture_check(c, player_id, world_id, towns, battle_points):
|
||||
"""Called after every state push.
|
||||
For each town that has auto_party or auto_triumph enabled:
|
||||
- skip if a pending/executing entry already exists in culture_queue
|
||||
- skip if celebration cooldown hasn't expired yet
|
||||
- skip if resources/battle-points are insufficient
|
||||
- otherwise insert a new 'auto' entry into culture_queue
|
||||
"""
|
||||
if not player_id or not world_id:
|
||||
return
|
||||
|
||||
now_ts = int(datetime.utcnow().timestamp())
|
||||
now_iso = datetime.utcnow().isoformat()
|
||||
bp_available = battle_points.get('available', 0) if isinstance(battle_points, dict) else 0
|
||||
|
||||
# Load auto settings for this player/world
|
||||
auto_rows = c.execute(
|
||||
'SELECT town_id, auto_party, auto_triumph FROM culture_settings WHERE player_id = ? AND world_id = ?',
|
||||
(player_id, world_id)
|
||||
).fetchall()
|
||||
if not auto_rows:
|
||||
return
|
||||
|
||||
# Build a quick lookup of town data from the state payload
|
||||
town_map = {str(t.get('town_id', '')): t for t in towns}
|
||||
|
||||
for auto_row in auto_rows:
|
||||
tid = str(auto_row['town_id'])
|
||||
td = town_map.get(tid)
|
||||
if not td:
|
||||
continue # town not in this state push (shouldn't happen but guard anyway)
|
||||
town_name = td.get('town_name', '')
|
||||
|
||||
for cel_type, enabled in [('party', auto_row['auto_party']), ('triumph', auto_row['auto_triumph'])]:
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
# 1. Already queued?
|
||||
existing = c.execute(
|
||||
"SELECT id FROM culture_queue WHERE player_id=? AND world_id=? AND celebration_type=? AND status IN ('pending','executing')",
|
||||
(player_id, world_id, cel_type)
|
||||
).fetchone()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
# 2. Cooldown still active?
|
||||
cel_cd = c.execute(
|
||||
'SELECT finished_at FROM celebrations WHERE player_id=? AND world_id=? AND town_id=? AND celebration_type=?',
|
||||
(player_id, world_id, tid, cel_type)
|
||||
).fetchone()
|
||||
if cel_cd and int(cel_cd['finished_at'] or 0) > now_ts:
|
||||
continue # still cooling down
|
||||
|
||||
# 3. Resources check
|
||||
if cel_type == 'party':
|
||||
if (td.get('wood', 0) < _PARTY_COST['wood'] or
|
||||
td.get('stone', 0) < _PARTY_COST['stone'] or
|
||||
td.get('iron', 0) < _PARTY_COST['iron']):
|
||||
continue # not enough yet — will retry on next state push
|
||||
else: # triumph
|
||||
if bp_available < _TRIUMPH_BP:
|
||||
continue
|
||||
|
||||
# All clear — queue it
|
||||
c.execute('''
|
||||
INSERT INTO culture_queue
|
||||
(player_id, world_id, town_id, town_name, celebration_type, status, source, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'pending', 'auto', ?)
|
||||
''', (player_id, world_id, tid, town_name, cel_type, now_iso))
|
||||
# Also log it
|
||||
cost_w = _PARTY_COST['wood'] if cel_type == 'party' else 0
|
||||
cost_s = _PARTY_COST['stone'] if cel_type == 'party' else 0
|
||||
cost_i = _PARTY_COST['iron'] if cel_type == 'party' else 0
|
||||
cost_b = _TRIUMPH_BP if cel_type == 'triumph' else 0
|
||||
c.execute('''
|
||||
INSERT INTO culture_log
|
||||
(player_id, world_id, town_id, town_name, celebration_type,
|
||||
cost_wood, cost_stone, cost_iron, cost_battle_pts, status, source, fired_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 'auto', ?)
|
||||
''', (player_id, world_id, tid, town_name, cel_type,
|
||||
cost_w, cost_s, cost_i, cost_b, now_iso))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -184,7 +277,7 @@ def _fetch_pending_of_type(c, cmd_type, player_id, world_id):
|
||||
|
||||
if not row:
|
||||
return None
|
||||
c.execute('''
|
||||
c.execute('''\
|
||||
UPDATE commands
|
||||
SET status = 'executing', updated_at = ?
|
||||
WHERE id = ?
|
||||
@@ -197,7 +290,58 @@ def _fetch_pending_of_type(c, cmd_type, player_id, world_id):
|
||||
}
|
||||
|
||||
|
||||
def _fetch_pending_culture(c, player_id, world_id):
|
||||
"""Fetch one pending culture command from the dedicated culture_queue table.
|
||||
Completely separate from the commands table — no interference with builds/recruits.
|
||||
Also times out stuck 'executing' rows to 'failed' after 5 minutes.
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
five_min_ago = (datetime.utcnow() - timedelta(minutes=5)).isoformat()
|
||||
|
||||
# Expire stuck executing entries (fail, don't requeue — auto will re-fire on next state push)
|
||||
c.execute('''
|
||||
UPDATE culture_queue
|
||||
SET status = 'failed', result_msg = 'Timeout (5 min)'
|
||||
WHERE status = 'executing' AND executed_at < ? AND player_id = ?
|
||||
''', (five_min_ago, player_id))
|
||||
|
||||
# Fetch one pending row
|
||||
if world_id:
|
||||
row = c.execute('''
|
||||
SELECT * FROM culture_queue
|
||||
WHERE status = 'pending' AND player_id = ? AND world_id = ?
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
''', (player_id, world_id)).fetchone()
|
||||
else:
|
||||
row = c.execute('''
|
||||
SELECT * FROM culture_queue
|
||||
WHERE status = 'pending' AND player_id = ?
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
''', (player_id,)).fetchone()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
c.execute('''
|
||||
UPDATE culture_queue SET status = 'executing', executed_at = ? WHERE id = ?
|
||||
''', (now, row['id']))
|
||||
|
||||
return {
|
||||
'id': row['id'],
|
||||
'town_id': row['town_id'],
|
||||
'type': 'culture',
|
||||
'payload': {
|
||||
'town_id': row['town_id'],
|
||||
'celebration_type': row['celebration_type'],
|
||||
'source': row['source']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _fetch_pending_builds_all_towns(c, player_id, world_id):
|
||||
|
||||
"""
|
||||
Fetch ONE pending 'build' command per distinct town_id.
|
||||
This allows all towns to build in parallel within a single poll cycle.
|
||||
@@ -298,7 +442,7 @@ def get_pending_command():
|
||||
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id, world_id)
|
||||
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id, world_id)
|
||||
research_cmd = _fetch_pending_of_type(c, 'research', player_id, world_id)
|
||||
culture_cmd = _fetch_pending_of_type(c, 'culture', player_id, world_id)
|
||||
culture_cmd = _fetch_pending_culture(c, player_id, world_id) # reads culture_queue
|
||||
sync_req = _check_and_reset_sync(c, player_id)
|
||||
|
||||
# Determine player_key for world-specific settings if world_id is provided
|
||||
@@ -573,9 +717,53 @@ def api_bot_logs():
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/bot
|
||||
# POST /api/culture/result/<id>
|
||||
# Bot reports success/failure of a culture command.
|
||||
# Completely separate from /api/commands/<id>/result — zero
|
||||
# interference with builds, recruits, or any other command type.
|
||||
# ------------------------------------------------------------------
|
||||
@api.route('/api/culture/result/<int:queue_id>', methods=['POST'])
|
||||
def culture_result(queue_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
status_in = data.get('status', 'done') # 'done' | 'failed'
|
||||
msg = data.get('message', '')
|
||||
now = datetime.utcnow().isoformat()
|
||||
final_status = 'done' if status_in == 'done' else 'failed'
|
||||
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
'SELECT * FROM culture_queue WHERE id = ?', (queue_id,)
|
||||
).fetchone()
|
||||
|
||||
if row:
|
||||
conn.execute('''
|
||||
UPDATE culture_queue
|
||||
SET status = ?, result_msg = ?, executed_at = ?
|
||||
WHERE id = ?
|
||||
''', (final_status, msg, now, queue_id))
|
||||
|
||||
# Sync the most recent matching pending culture_log entry
|
||||
log_row = conn.execute('''
|
||||
SELECT id FROM culture_log
|
||||
WHERE player_id = ? AND world_id = ? AND town_id = ?
|
||||
AND celebration_type = ? AND status = 'pending'
|
||||
ORDER BY id DESC LIMIT 1
|
||||
''', (row['player_id'], row['world_id'], row['town_id'], row['celebration_type'])).fetchone()
|
||||
if log_row:
|
||||
log_status = 'success' if final_status == 'done' else 'failed'
|
||||
conn.execute('''
|
||||
UPDATE culture_log
|
||||
SET status = ?, result_msg = ?, confirmed_at = ?
|
||||
WHERE id = ?
|
||||
''', (log_status, msg, now, log_row['id']))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
|
||||
# Serves the modular bot code concatenated into a single response
|
||||
# ------------------------------------------------------------------
|
||||
@api.route('/api/bot', methods=['GET'])
|
||||
|
||||
@@ -731,6 +731,17 @@ def get_agora():
|
||||
for r in cel_rows:
|
||||
cooldowns[(r['town_id'], r['celebration_type'])] = r['finished_at']
|
||||
|
||||
# ── Auto settings ────────────────────────────────────────
|
||||
settings_rows = conn.execute('''
|
||||
SELECT town_id, auto_party, auto_triumph
|
||||
FROM culture_settings
|
||||
WHERE player_id = ? AND world_id = ?
|
||||
''', (player_id, world_id)).fetchall()
|
||||
auto_settings = {} # town_id → {auto_party, auto_triumph}
|
||||
for s in settings_rows:
|
||||
auto_settings[(s['town_id'], 'auto_party')] = s['auto_party']
|
||||
auto_settings[(s['town_id'], 'auto_triumph')] = s['auto_triumph']
|
||||
|
||||
conn.close()
|
||||
|
||||
# ── Per-town eligibility ─────────────────────────────────────────
|
||||
@@ -801,6 +812,8 @@ def get_agora():
|
||||
'available': triumph_ok,
|
||||
'cooldown_until': triumph_cd or None,
|
||||
},
|
||||
'auto_party': bool(auto_settings.get((tid, 'auto_party'), 0)),
|
||||
'auto_triumph': bool(auto_settings.get((tid, 'auto_triumph'), 0)),
|
||||
})
|
||||
|
||||
return jsonify({'towns': towns_out, 'costs': {'party': PARTY_COST, 'triumph': TRIUMPH_COST}})
|
||||
@@ -824,7 +837,7 @@ def _fmt_seconds(secs):
|
||||
# Body: { player_id, world_id, town_id, town_name, celebration_type }
|
||||
# • Validates client is online.
|
||||
# • Validates eligibility (resources / battle_points / cooldown).
|
||||
# • Inserts a 'culture' command into the commands queue.
|
||||
# • Inserts a 'culture' command into the culture_queue.
|
||||
# • Inserts a 'pending' row into culture_log.
|
||||
# ------------------------------------------------------------------
|
||||
@dashboard.route('/dashboard/culture-command', methods=['POST'])
|
||||
@@ -902,32 +915,43 @@ def culture_command():
|
||||
return jsonify({'error': 'insufficient_battle_points',
|
||||
'message': f'Ανεπαρκείς πόντοι μάχης ({available_bp}/{TRIUMPH_COST}).'}), 409
|
||||
|
||||
# ── Insert command into bot queue ────────────────────────────────
|
||||
now_iso = datetime.utcnow().isoformat()
|
||||
# ── Insert into culture_queue ────────────────────────────────
|
||||
c = conn.cursor()
|
||||
|
||||
# One-at-a-time check: reject if this type already pending/executing
|
||||
existing = c.execute('''
|
||||
SELECT id FROM culture_queue
|
||||
WHERE player_id = ? AND world_id = ? AND town_id = ? AND celebration_type = ?
|
||||
AND status IN ('pending', 'executing')
|
||||
''', (str(player_id), world_id, town_id, cel_type)).fetchone()
|
||||
if existing:
|
||||
conn.close()
|
||||
label = 'Γιορτή πόλης' if cel_type == 'party' else 'Παρέλαση θριάμβου'
|
||||
return jsonify({'error': 'already_queued',
|
||||
'message': f'{label} για την πόλη αυτή είναι ήδη σε αναμονή εκτέλεσης.'}), 409
|
||||
|
||||
now_iso = datetime.utcnow().isoformat()
|
||||
|
||||
c.execute('''
|
||||
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
|
||||
VALUES (?, ?, 'culture', ?, 'pending', ?, ?, ?)
|
||||
''', (
|
||||
town_id, town_name,
|
||||
json.dumps({'celebration_type': cel_type, 'town_id': int(town_id)}),
|
||||
now_iso, now_iso, str(player_id)
|
||||
))
|
||||
cmd_id = c.lastrowid
|
||||
INSERT INTO culture_queue
|
||||
(player_id, world_id, town_id, town_name, celebration_type, status, source, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'pending', 'manual', ?)
|
||||
''', (str(player_id), world_id, town_id, town_name, cel_type, now_iso))
|
||||
queue_id = c.lastrowid
|
||||
|
||||
# ── Append culture_log row (status=pending until bot confirms) ───
|
||||
c.execute('''
|
||||
INSERT INTO culture_log
|
||||
(player_id, world_id, town_id, town_name, celebration_type,
|
||||
cost_wood, cost_stone, cost_iron, cost_battle_pts, status, fired_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)
|
||||
cost_wood, cost_stone, cost_iron, cost_battle_pts, status, source, fired_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 'manual', ?)
|
||||
''', (str(player_id), world_id, town_id, town_name, cel_type,
|
||||
cost_wood, cost_stone, cost_iron, cost_bp, now_iso))
|
||||
log_id = c.lastrowid
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'ok': True, 'cmd_id': cmd_id, 'log_id': log_id})
|
||||
return jsonify({'ok': True, 'queue_id': queue_id, 'log_id': log_id})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -946,7 +970,7 @@ def get_culture_log():
|
||||
rows = conn.execute('''
|
||||
SELECT id, town_id, town_name, celebration_type,
|
||||
cost_wood, cost_stone, cost_iron, cost_battle_pts,
|
||||
status, result_msg, fired_at, confirmed_at
|
||||
status, result_msg, source, fired_at, confirmed_at
|
||||
FROM culture_log
|
||||
WHERE player_id = ? AND world_id = ?
|
||||
ORDER BY id DESC
|
||||
@@ -954,3 +978,64 @@ def get_culture_log():
|
||||
''', (player_id, world_id)).fetchall()
|
||||
conn.close()
|
||||
return jsonify([dict(r) for r in rows])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /dashboard/culture-settings?player_id=&world_id=&town_id=
|
||||
# POST /dashboard/culture-settings
|
||||
# Body: { player_id, world_id, town_id, auto_party, auto_triumph }
|
||||
# Save/retrieve per-town auto-celebration preferences.
|
||||
# ------------------------------------------------------------------
|
||||
@dashboard.route('/dashboard/culture-settings', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def culture_settings():
|
||||
if request.method == 'GET':
|
||||
player_id = request.args.get('player_id')
|
||||
world_id = request.args.get('world_id', '')
|
||||
town_id = request.args.get('town_id', '')
|
||||
if not player_id:
|
||||
return jsonify({'error': 'missing player_id'}), 400
|
||||
|
||||
conn = get_db()
|
||||
if town_id:
|
||||
row = conn.execute(
|
||||
'SELECT auto_party, auto_triumph FROM culture_settings WHERE player_id=? AND world_id=? AND town_id=?',
|
||||
(player_id, world_id, town_id)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if row:
|
||||
return jsonify({'auto_party': bool(row['auto_party']), 'auto_triumph': bool(row['auto_triumph'])})
|
||||
return jsonify({'auto_party': False, 'auto_triumph': False})
|
||||
else:
|
||||
rows = conn.execute(
|
||||
'SELECT town_id, auto_party, auto_triumph FROM culture_settings WHERE player_id=? AND world_id=?',
|
||||
(player_id, world_id)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return jsonify([dict(r) for r in rows])
|
||||
|
||||
# POST — save settings
|
||||
data = request.get_json(silent=True) or {}
|
||||
player_id = data.get('player_id')
|
||||
world_id = data.get('world_id', '')
|
||||
town_id = str(data.get('town_id', ''))
|
||||
auto_party = 1 if data.get('auto_party') else 0
|
||||
auto_triumph = 1 if data.get('auto_triumph') else 0
|
||||
|
||||
if not player_id or not town_id:
|
||||
return jsonify({'error': 'missing player_id or town_id'}), 400
|
||||
|
||||
now_iso = datetime.utcnow().isoformat()
|
||||
conn = get_db()
|
||||
conn.execute('''
|
||||
INSERT INTO culture_settings (player_id, world_id, town_id, auto_party, auto_triumph, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(player_id, world_id, town_id) DO UPDATE SET
|
||||
auto_party = excluded.auto_party,
|
||||
auto_triumph = excluded.auto_triumph,
|
||||
updated_at = excluded.updated_at
|
||||
''', (str(player_id), world_id, town_id, auto_party, auto_triumph, now_iso))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'ok': True, 'auto_party': bool(auto_party), 'auto_triumph': bool(auto_triumph)})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user