fix que / add auto
This commit is contained in:
@@ -1,40 +1,52 @@
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// 04d_execute_culture.js — Execute Αγορά celebration commands
|
// 04d_execute_culture.js — Execute Αγορά celebration commands
|
||||||
//
|
//
|
||||||
// Handles commands of type 'culture' from the poll response.
|
// Handles commands of type 'culture' from the culture_queue.
|
||||||
// Supported celebration_type values:
|
// Completely separate from the commands table — builds/recruits
|
||||||
// 'party' → Γιορτή πόλης (costs wood/stone/iron)
|
// are never blocked by a stuck culture command.
|
||||||
// 'triumph' → Παρέλαση θριάμβου (costs battle points)
|
|
||||||
//
|
//
|
||||||
// Uses the same fire-and-forget ajaxPost pattern as all other
|
// Reports results to /api/culture/result/<id> (dedicated endpoint).
|
||||||
// execute modules in this codebase (04a, 04b, 04c).
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
async function executeCultureCommand(cmd) {
|
async function reportCultureResult(queueId, status, msg) {
|
||||||
if (!cmd || !cmd.id || !cmd.payload) return;
|
try {
|
||||||
|
await apiFetch(`${BASE_URL}/api/culture/result/${queueId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status, message: msg })
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
log(`[αγορά] ⚠️ Failed to report result for queue #${queueId}: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// town_id lives on the command root (like build/recruit), payload has celebration_type
|
async function executeCultureCommand(cmd) {
|
||||||
const town_id = cmd.town_id || cmd.payload.town_id;
|
if (!cmd || !cmd.id || !cmd.payload) return { ok: false, msg: 'No payload' };
|
||||||
|
|
||||||
|
// town_id lives on the command root; payload has celebration_type
|
||||||
|
const town_id = cmd.town_id || cmd.payload.town_id;
|
||||||
const celebration_type = cmd.payload.celebration_type;
|
const celebration_type = cmd.payload.celebration_type;
|
||||||
|
const source = cmd.payload.source || 'manual';
|
||||||
|
|
||||||
if (!celebration_type || !town_id) {
|
if (!celebration_type || !town_id) {
|
||||||
reportResult(cmd.id, 'failed', 'Invalid culture payload: missing celebration_type or town_id');
|
await reportCultureResult(cmd.id, 'failed', 'Invalid culture payload: missing celebration_type or town_id');
|
||||||
return;
|
return { ok: false, msg: 'Invalid payload' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['party', 'triumph'].includes(celebration_type)) {
|
if (!['party', 'triumph'].includes(celebration_type)) {
|
||||||
reportResult(cmd.id, 'failed', `Unknown celebration_type: ${celebration_type}`);
|
await reportCultureResult(cmd.id, 'failed', `Unknown celebration_type: ${celebration_type}`);
|
||||||
return;
|
return { ok: false, msg: 'Unknown type' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = celebration_type === 'party' ? 'Γιορτή πόλης' : 'Παρέλαση θριάμβου';
|
const label = celebration_type === 'party' ? 'Γιορτή πόλης' : 'Παρέλαση θριάμβου';
|
||||||
log(`[αγορά] Εκτέλεση ${label} για πόλη ${town_id}`);
|
log(`[αγορά] Εκτέλεση ${label} για πόλη ${town_id} (${source})`);
|
||||||
|
|
||||||
// Validate town exists in game memory
|
// Validate town exists in game memory
|
||||||
const town = uw.ITowns?.towns?.[town_id];
|
const town = uw.ITowns?.towns?.[town_id];
|
||||||
if (!town) {
|
if (!town) {
|
||||||
reportResult(cmd.id, 'failed', `Η πόλη ${town_id} δεν βρέθηκε στη μνήμη του παιχνιδιού.`);
|
const msg = `Η πόλη ${town_id} δεν βρέθηκε στη μνήμη του παιχνιδιού.`;
|
||||||
return;
|
await reportCultureResult(cmd.id, 'failed', msg);
|
||||||
|
return { ok: false, msg };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check: is there already a celebration of this type running?
|
// Double-check: is there already a celebration of this type running?
|
||||||
@@ -47,9 +59,9 @@ async function executeCultureCommand(cmd) {
|
|||||||
if (String(a.town_id) === String(town_id)
|
if (String(a.town_id) === String(town_id)
|
||||||
&& a.celebration_type === celebration_type
|
&& a.celebration_type === celebration_type
|
||||||
&& (a.finished_at ?? 0) > nowTs) {
|
&& (a.finished_at ?? 0) > nowTs) {
|
||||||
reportResult(cmd.id, 'failed',
|
const msg = `${label} ήδη ενεργή στην πόλη ${town_id}.`;
|
||||||
`${label} ήδη ενεργή στην πόλη ${town_id}.`);
|
await reportCultureResult(cmd.id, 'failed', msg);
|
||||||
return;
|
return { ok: false, msg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,8 +73,8 @@ async function executeCultureCommand(cmd) {
|
|||||||
await sleep(reactionMs);
|
await sleep(reactionMs);
|
||||||
|
|
||||||
if (paused) {
|
if (paused) {
|
||||||
reportResult(cmd.id, 'failed', 'Aborted due to pause/captcha');
|
await reportCultureResult(cmd.id, 'failed', 'Aborted due to pause/captcha');
|
||||||
return;
|
return { ok: false, msg: 'Paused' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire-and-forget — exact same 3-arg pattern used everywhere in this codebase.
|
// Fire-and-forget — exact same 3-arg pattern used everywhere in this codebase.
|
||||||
@@ -74,6 +86,8 @@ async function executeCultureCommand(cmd) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
log(`[αγορά] ✅ ${label} εστάλη (πόλη ${town_id})`);
|
const successMsg = `${label} εστάλη επιτυχώς (${source}).`;
|
||||||
reportResult(cmd.id, 'done', `${label} εστάλη επιτυχώς.`);
|
log(`[αγορά] ✅ ${successMsg} (πόλη ${town_id})`);
|
||||||
|
await reportCultureResult(cmd.id, 'done', successMsg);
|
||||||
|
return { ok: true, msg: successMsg };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,12 @@ async function pollAndExecute() {
|
|||||||
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
||||||
else if (cmd.type === 'research') result = await executeResearch(cmd);
|
else if (cmd.type === 'research') result = await executeResearch(cmd);
|
||||||
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
|
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
|
||||||
else if (cmd.type === 'culture') result = await executeCultureCommand(cmd);
|
else if (cmd.type === 'culture') {
|
||||||
|
// executeCultureCommand reports to /api/culture/result directly
|
||||||
|
// — do NOT call the shared reportResult() afterwards
|
||||||
|
await executeCultureCommand(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
else if (cmd.type === 'farm_loot') {
|
else if (cmd.type === 'farm_loot') {
|
||||||
// Guard: if auto-farm is mid-run, requeue rather than overlap
|
// Guard: if auto-farm is mid-run, requeue rather than overlap
|
||||||
if (farmLootRunning) {
|
if (farmLootRunning) {
|
||||||
|
|||||||
38
db.py
38
db.py
@@ -126,6 +126,7 @@ def init_db():
|
|||||||
cost_iron INTEGER NOT NULL DEFAULT 0,
|
cost_iron INTEGER NOT NULL DEFAULT 0,
|
||||||
cost_battle_pts INTEGER NOT NULL DEFAULT 0,
|
cost_battle_pts INTEGER NOT NULL DEFAULT 0,
|
||||||
status TEXT NOT NULL DEFAULT 'pending', -- pending | success | failed
|
status TEXT NOT NULL DEFAULT 'pending', -- pending | success | failed
|
||||||
|
source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'auto'
|
||||||
result_msg TEXT,
|
result_msg TEXT,
|
||||||
fired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
fired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
confirmed_at TEXT
|
confirmed_at TEXT
|
||||||
@@ -133,6 +134,43 @@ def init_db():
|
|||||||
''')
|
''')
|
||||||
c.execute('CREATE INDEX IF NOT EXISTS idx_culture_log_player_world ON culture_log(player_id, world_id)')
|
c.execute('CREATE INDEX IF NOT EXISTS idx_culture_log_player_world ON culture_log(player_id, world_id)')
|
||||||
|
|
||||||
|
# Culture queue — dedicated queue for celebration commands (separate from commands table).
|
||||||
|
# Max 1 pending/executing per player+world+celebration_type enforced at app level.
|
||||||
|
# source: 'manual' (dashboard button) | 'auto' (server-side auto-fire from state push)
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS culture_queue (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_id TEXT NOT NULL,
|
||||||
|
world_id TEXT NOT NULL,
|
||||||
|
town_id TEXT NOT NULL,
|
||||||
|
town_name TEXT NOT NULL DEFAULT '',
|
||||||
|
celebration_type TEXT NOT NULL, -- 'party' | 'triumph'
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
|
||||||
|
source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'auto'
|
||||||
|
result_msg TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
executed_at TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
c.execute('CREATE INDEX IF NOT EXISTS idx_culture_queue_player_world ON culture_queue(player_id, world_id, status)')
|
||||||
|
|
||||||
|
# Culture settings — per-town auto-mode toggle (party / triumph).
|
||||||
|
# One row per player+world+town. Updated from Αγορά dashboard toggles.
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS culture_settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_id TEXT NOT NULL,
|
||||||
|
world_id TEXT NOT NULL,
|
||||||
|
town_id TEXT NOT NULL,
|
||||||
|
auto_party INTEGER NOT NULL DEFAULT 0,
|
||||||
|
auto_triumph INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(player_id, world_id, town_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
c.execute('CREATE INDEX IF NOT EXISTS idx_culture_settings_player_world ON culture_settings(player_id, world_id)')
|
||||||
|
|
||||||
|
|
||||||
# Blueprints - assigns a blueprint to a specific town
|
# Blueprints - assigns a blueprint to a specific town
|
||||||
c.execute('''
|
c.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS town_blueprints (
|
CREATE TABLE IF NOT EXISTS town_blueprints (
|
||||||
|
|||||||
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))
|
''', (str(player_id), world_id, town_id_cel, t_name, cel_type, finished_at, now_iso))
|
||||||
conn.commit()
|
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()
|
conn.close()
|
||||||
return jsonify({'ok': True, 'towns_updated': len(towns)})
|
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:
|
if not row:
|
||||||
return None
|
return None
|
||||||
c.execute('''
|
c.execute('''\
|
||||||
UPDATE commands
|
UPDATE commands
|
||||||
SET status = 'executing', updated_at = ?
|
SET status = 'executing', updated_at = ?
|
||||||
WHERE id = ?
|
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):
|
def _fetch_pending_builds_all_towns(c, player_id, world_id):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Fetch ONE pending 'build' command per distinct town_id.
|
Fetch ONE pending 'build' command per distinct town_id.
|
||||||
This allows all towns to build in parallel within a single poll cycle.
|
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_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)
|
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)
|
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)
|
sync_req = _check_and_reset_sync(c, player_id)
|
||||||
|
|
||||||
# Determine player_key for world-specific settings if world_id is provided
|
# Determine player_key for world-specific settings if world_id is provided
|
||||||
@@ -573,9 +717,53 @@ def api_bot_logs():
|
|||||||
return jsonify({'ok': True})
|
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
|
# Serves the modular bot code concatenated into a single response
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@api.route('/api/bot', methods=['GET'])
|
@api.route('/api/bot', methods=['GET'])
|
||||||
|
|||||||
@@ -731,6 +731,17 @@ def get_agora():
|
|||||||
for r in cel_rows:
|
for r in cel_rows:
|
||||||
cooldowns[(r['town_id'], r['celebration_type'])] = r['finished_at']
|
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()
|
conn.close()
|
||||||
|
|
||||||
# ── Per-town eligibility ─────────────────────────────────────────
|
# ── Per-town eligibility ─────────────────────────────────────────
|
||||||
@@ -801,6 +812,8 @@ def get_agora():
|
|||||||
'available': triumph_ok,
|
'available': triumph_ok,
|
||||||
'cooldown_until': triumph_cd or None,
|
'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}})
|
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 }
|
# Body: { player_id, world_id, town_id, town_name, celebration_type }
|
||||||
# • Validates client is online.
|
# • Validates client is online.
|
||||||
# • Validates eligibility (resources / battle_points / cooldown).
|
# • 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.
|
# • Inserts a 'pending' row into culture_log.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@dashboard.route('/dashboard/culture-command', methods=['POST'])
|
@dashboard.route('/dashboard/culture-command', methods=['POST'])
|
||||||
@@ -902,32 +915,43 @@ def culture_command():
|
|||||||
return jsonify({'error': 'insufficient_battle_points',
|
return jsonify({'error': 'insufficient_battle_points',
|
||||||
'message': f'Ανεπαρκείς πόντοι μάχης ({available_bp}/{TRIUMPH_COST}).'}), 409
|
'message': f'Ανεπαρκείς πόντοι μάχης ({available_bp}/{TRIUMPH_COST}).'}), 409
|
||||||
|
|
||||||
# ── Insert command into bot queue ────────────────────────────────
|
# ── Insert into culture_queue ────────────────────────────────
|
||||||
now_iso = datetime.utcnow().isoformat()
|
|
||||||
c = conn.cursor()
|
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('''
|
c.execute('''
|
||||||
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
|
INSERT INTO culture_queue
|
||||||
VALUES (?, ?, 'culture', ?, 'pending', ?, ?, ?)
|
(player_id, world_id, town_id, town_name, celebration_type, status, source, created_at)
|
||||||
''', (
|
VALUES (?, ?, ?, ?, ?, 'pending', 'manual', ?)
|
||||||
town_id, town_name,
|
''', (str(player_id), world_id, town_id, town_name, cel_type, now_iso))
|
||||||
json.dumps({'celebration_type': cel_type, 'town_id': int(town_id)}),
|
queue_id = c.lastrowid
|
||||||
now_iso, now_iso, str(player_id)
|
|
||||||
))
|
|
||||||
cmd_id = c.lastrowid
|
|
||||||
|
|
||||||
# ── Append culture_log row (status=pending until bot confirms) ───
|
# ── Append culture_log row (status=pending until bot confirms) ───
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO culture_log
|
INSERT INTO culture_log
|
||||||
(player_id, world_id, town_id, town_name, celebration_type,
|
(player_id, world_id, town_id, town_name, celebration_type,
|
||||||
cost_wood, cost_stone, cost_iron, cost_battle_pts, status, fired_at)
|
cost_wood, cost_stone, cost_iron, cost_battle_pts, status, source, fired_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 'manual', ?)
|
||||||
''', (str(player_id), world_id, town_id, town_name, cel_type,
|
''', (str(player_id), world_id, town_id, town_name, cel_type,
|
||||||
cost_wood, cost_stone, cost_iron, cost_bp, now_iso))
|
cost_wood, cost_stone, cost_iron, cost_bp, now_iso))
|
||||||
log_id = c.lastrowid
|
log_id = c.lastrowid
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
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('''
|
rows = conn.execute('''
|
||||||
SELECT id, town_id, town_name, celebration_type,
|
SELECT id, town_id, town_name, celebration_type,
|
||||||
cost_wood, cost_stone, cost_iron, cost_battle_pts,
|
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
|
FROM culture_log
|
||||||
WHERE player_id = ? AND world_id = ?
|
WHERE player_id = ? AND world_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
@@ -954,3 +978,64 @@ def get_culture_log():
|
|||||||
''', (player_id, world_id)).fetchall()
|
''', (player_id, world_id)).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify([dict(r) for r in rows])
|
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)})
|
||||||
|
|
||||||
|
|||||||
@@ -343,6 +343,39 @@
|
|||||||
.btn-confirm:hover { background: #c99ef0; }
|
.btn-confirm:hover { background: #c99ef0; }
|
||||||
.btn-confirm:disabled { background: #444; color: var(--muted); cursor: not-allowed; }
|
.btn-confirm:disabled { background: #444; color: var(--muted); cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* ── Auto toggle ── */
|
||||||
|
.auto-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.auto-toggle button {
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: none;
|
||||||
|
background: #1e1e30;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .2s;
|
||||||
|
}
|
||||||
|
.auto-toggle button.active-manual { background: #2a2a40; color: var(--text); }
|
||||||
|
.auto-toggle button.active-auto { background: rgba(180,130,220,0.25); color: var(--purple); }
|
||||||
|
.cel-card.auto-on { border-color: rgba(180,130,220,0.6); box-shadow: 0 0 12px rgba(180,130,220,0.08); }
|
||||||
|
.auto-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: rgba(180,130,220,0.15);
|
||||||
|
color: var(--purple);
|
||||||
|
border: 1px solid rgba(180,130,220,0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1.5rem;
|
bottom: 1.5rem;
|
||||||
@@ -488,8 +521,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function celebCard(town, type) {
|
function celebCard(town, type) {
|
||||||
const cel = town[type];
|
const cel = town[type];
|
||||||
const label = TYPE_LABELS[type];
|
const label = TYPE_LABELS[type];
|
||||||
|
const autoKey = type === 'party' ? 'auto_party' : 'auto_triumph';
|
||||||
|
const autoOn = !!town[autoKey];
|
||||||
|
|
||||||
let costsHtml = '';
|
let costsHtml = '';
|
||||||
if (type === 'party') {
|
if (type === 'party') {
|
||||||
@@ -512,19 +547,33 @@
|
|||||||
? `<div class="reason-box ${cel.status === 'cooldown' ? '' : 'error'}">${cel.reason}</div>`
|
? `<div class="reason-box ${cel.status === 'cooldown' ? '' : 'error'}">${cel.reason}</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const btnLabel = cel.status === 'cooldown' ? `⏰ ${cel.reason}`
|
const autoToggle = `
|
||||||
: cel.status === 'unavailable' ? '✖ Μη διαθέσιμο'
|
<div class="auto-toggle" title="Αυτόματη εκτέλεση όταν ξεκλειδώσουν πόροι/cooldown">
|
||||||
: `▶ Εκκίνηση ${label}`;
|
<button id="btn-manual-${town.town_id}-${type}"
|
||||||
|
class="${!autoOn ? 'active-manual' : ''}"
|
||||||
|
onclick="saveAutoSetting('${town.town_id}','${type}',false)">
|
||||||
|
Χειροκίνητο
|
||||||
|
</button>
|
||||||
|
<button id="btn-auto-${town.town_id}-${type}"
|
||||||
|
class="${autoOn ? 'active-auto' : ''}"
|
||||||
|
onclick="saveAutoSetting('${town.town_id}','${type}',true)">
|
||||||
|
⚡ Αυτόματο
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="cel-card ${cel.status}">
|
<div class="cel-card ${cel.status}${autoOn ? ' auto-on' : ''}">
|
||||||
<div class="cel-card-title">${label}</div>
|
<div class="cel-card-title">
|
||||||
|
${label}
|
||||||
|
${autoOn ? '<span class="auto-badge">AUTO</span>' : ''}
|
||||||
|
</div>
|
||||||
${costsHtml}
|
${costsHtml}
|
||||||
${reasonHtml}
|
${reasonHtml}
|
||||||
|
${autoToggle}
|
||||||
<button class="cel-btn primary"
|
<button class="cel-btn primary"
|
||||||
${cel.available ? '' : 'disabled'}
|
${(cel.available && !autoOn) ? '' : 'disabled'}
|
||||||
onclick="openModal('${town.town_id}','${town.town_name}','${type}')">
|
onclick="openModal('${town.town_id}','${town.town_name}','${type}')">
|
||||||
${cel.available ? `▶ Εκκίνηση` : (cel.status === 'cooldown' ? `⏰ Σε αναμονή` : `✖ Μη διαθέσιμο`)}
|
${autoOn ? '⚡ Αυτόματο ενεργό' : cel.available ? '▶ Εκκίνηση' : (cel.status === 'cooldown' ? '⏰ Σε αναμονή' : '✖ Μη διαθέσιμο')}
|
||||||
</button>
|
</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -534,6 +583,37 @@
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Auto-setting toggle ───────────────────────────────────
|
||||||
|
async function saveAutoSetting(townId, type, enable) {
|
||||||
|
const town = agoraData.find(t => t.town_id === townId);
|
||||||
|
if (!town) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
player_id: PLAYER_ID,
|
||||||
|
world_id: WORLD_ID,
|
||||||
|
town_id: townId,
|
||||||
|
auto_party: type === 'party' ? enable : !!town.auto_party,
|
||||||
|
auto_triumph: type === 'triumph' ? enable : !!town.auto_triumph
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/dashboard/culture-settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.ok) {
|
||||||
|
// Optimistically update local state and re-render card
|
||||||
|
town[type === 'party' ? 'auto_party' : 'auto_triumph'] = enable;
|
||||||
|
renderCards(town);
|
||||||
|
showToast(enable ? `⚡ Αυτόματο ενεργό — ${TYPE_LABELS[type]}` : `✓ Χειροκίνητο — ${TYPE_LABELS[type]}`, 'ok');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast(`❌ Αποτυχία αποθήκευσης: ${e}`, 'err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Modal ────────────────────────────────────────────────────────
|
// ── Modal ────────────────────────────────────────────────────────
|
||||||
function openModal(townId, townName, celType) {
|
function openModal(townId, townName, celType) {
|
||||||
pendingCmd = { townId, townName, celType };
|
pendingCmd = { townId, townName, celType };
|
||||||
@@ -633,7 +713,10 @@
|
|||||||
<span class="log-town">${row.town_name}</span>
|
<span class="log-town">${row.town_name}</span>
|
||||||
<span class="log-type">${TYPE_LABELS[row.celebration_type] || row.celebration_type}</span>
|
<span class="log-type">${TYPE_LABELS[row.celebration_type] || row.celebration_type}</span>
|
||||||
<span class="log-status ${row.status}">${row.status === 'success' ? '✅' : row.status === 'failed' ? '❌' : '⏳'}</span>
|
<span class="log-status ${row.status}">${row.status === 'success' ? '✅' : row.status === 'failed' ? '❌' : '⏳'}</span>
|
||||||
<span class="log-msg">${costStr}${row.result_msg ? ' — ' + row.result_msg : ''}</span>`;
|
<span class="log-msg">
|
||||||
|
${row.source === 'auto' ? '<span class="auto-badge" style="margin-right:4px">AUTO</span>' : ''}
|
||||||
|
${costStr}${row.result_msg ? ' — ' + row.result_msg : ''}
|
||||||
|
</span>`;
|
||||||
body.insertBefore(el, body.firstChild);
|
body.insertBefore(el, body.firstChild);
|
||||||
});
|
});
|
||||||
} catch (e) { /* silent */ }
|
} catch (e) { /* silent */ }
|
||||||
|
|||||||
Reference in New Issue
Block a user