Corsair - Command Manager

Newbie Spellweaver
Joined
Nov 24, 2023
Messages
20
Reaction score
20
I wanted to be able to teleport or use commands and stuff in game.
I found that using the role-play channel, chat is logged.
I found a way to read the log and react based on that.

Currently, I have it set up where you can execute a few commands:
rescue = Sends a teleport item for Velia
tp velia = Sends a teleport item for Velia

tp followed by: heidel, grana, altinova, aakman, Duvencrune, crescent, harsh, hystria, pirate, sycraia, valencia
Will send you an item, via mail, that will teleport you to those locations. Its quite nice.

1. You will need to install python. I'm using 3.9.5 because its installed with Corsair.
2. You will need sendmail (in this same forum, I have a post about Central/Market, Black Market. Under the python code, is how to set up sendMail.

Technically, the sky is the limit. You can enter a command into role-play chat and have this program send you whatever.



Python:
# ServerCommandManager.py
# Watches World Chat (type 15). Commands like: "tp Heidel" -> send mail with mapped item.
# Requires: pip install pyodbc

import time
import threading
from datetime import datetime, timedelta
import pyodbc
import re

# ========= CONFIG =========
SQL_SERVER = r"localhost"                           # change if remote
SQL_DRIVER = r"ODBC Driver 17 for SQL Server"       # or 18/11 as installed
USE_TRUSTED = True
SQL_USER = "CrimsonDesert"
SQL_PASS = "BlackNo.1Game#^%"

DB_LOG   = "PF_BETA_LOGDB_0001"
DB_WORLD = "SA_BETA_WORLDDB_0002"

POLL_SEC = 0.5

MAIL_ENCHANT = 0
MAIL_TITLE   = "Command Mail Express"
MAIL_BODY    = "Mail sent from the command systems."

# Command library: city keyword (lowercase) -> itemKey (count is always 1)
TP_ITEMS = {
    "velia": 43230, 
    "heidel": 43231, 
    "grana": 43232,
    "altinova": 43233,
    "duvencrune": 43234,
    "aakman": 42491,
    "crescent": 42446,
    "hasrah": 43297,
    "hystria": 42492,
    "pirate": 42439,
    "sycraia": 42265,
    "valencia": 42410
    # etc.
}

# ========= SQL HELPERS =========
def make_conn(dbname: str) -> pyodbc.Connection:
    if USE_TRUSTED:
        cs = f"DRIVER={{{SQL_DRIVER}}};SERVER={SQL_SERVER};DATABASE={dbname};Trusted_Connection=yes"
    else:
        cs = f"DRIVER={{{SQL_DRIVER}}};SERVER={SQL_SERVER};DATABASE={dbname};UID={SQL_USER};PWD={SQL_PASS}"
    return pyodbc.connect(cs, autocommit=True)

# ========= WATCHER =========
class ChatCommandWatcher:
    CMD_TP_RE = re.compile(r"^\s*tp\s+([A-Za-z]+)\s*$", re.IGNORECASE)
    CMD_RESCUE_RE = re.compile(r"^\s*rescue\s*$", re.IGNORECASE)

    def __init__(self):
        self.stop = threading.Event()
        self.last_seen_ts = datetime.utcnow() - timedelta(milliseconds=500)
        self.seen = set()  # (ts, userNo, msg)
        self.cooldown = {}  # userNo -> datetime of last tp
        self.COOLDOWN_SEC = 5 * 60

        self.rescue_cd = {}         # for rescue
        self.RESCUE_COOLDOWN_SEC = 5 * 60

        self.cn_log = make_conn(DB_LOG)
        self.cn_world = make_conn(DB_WORLD)

    def run(self):
        print("Listening for Roleplay Chat (type=15): commands 'tp <city>' and 'rescue'...")
        try:
            while not self.stop.is_set():
                self.tick()
                time.sleep(POLL_SEC)
        finally:
            self.close()

    def tick(self):
        q = f"""
        SELECT TOP (200)
            _registerDate, _chatMessage, _characterName, _characterNo, _userNo
        FROM {DB_LOG}.PaGamePrivate.TblChatLog_INMEM WITH (READUNCOMMITTED)
        WHERE _chatType = 15
          AND _registerDate >= ?
        ORDER BY _registerDate ASC;
        """
        try:
            cur = self.cn_log.cursor()
            cur.execute(q, self.last_seen_ts)
            rows = cur.fetchall()
        except Exception as e:
            print(f"[ERR] chat query failed: {e}")
            return

        newest_ts = None
        for ts, msg, char_name, char_no, user_no in rows:
            key = (ts, int(user_no or 0), str(msg or ""))
            if key in self.seen:
                continue
            self.seen.add(key)

            if msg:
                self.process_message(ts, str(msg), int(user_no or 0), char_name, char_no)

            if newest_ts is None or (ts and ts > newest_ts):
                newest_ts = ts

        if newest_ts and newest_ts >= self.last_seen_ts:
            self.last_seen_ts = newest_ts + timedelta(milliseconds=1)

    def process_message(self, ts, msg, user_no, char_name, char_no):
        uid = int(user_no or 0)
        now = datetime.utcnow()

        # --- rescue (5‑min cooldown) ---
        if self.CMD_RESCUE_RE.match(msg):
            last = self.rescue_cd.get(uid)
            if last and (now - last).total_seconds() < self.RESCUE_COOLDOWN_SEC:
                remain = int(self.RESCUE_COOLDOWN_SEC - (now - last).total_seconds())
                print(f"[CD] user {uid} rescue blocked: {remain}s remaining")
                return

            family = self.lookup_family_name_by_userno(uid)
            if not family:
                print(f"[WARN] rescue: cannot resolve family for userNo={uid}")
                return

            symno = self.send_mail(family, 43230, 1)  # Map to Velia
            if symno:
                self.rescue_cd[uid] = now
                print(f"[OK] {ts} rescue -> mailed Velia map (42563) to @{family} symNo={symno}")
            else:
                print(f"[ERR] sendMail failed for @{family} (rescue)")
            return

        # --- tp <city> (5‑min cooldown) ---
        m = self.CMD_TP_RE.match(msg)
        if not m:
            return
        city = m.group(1).lower()

        last = self.cooldown.get(uid)
        if last and (now - last).total_seconds() < self.COOLDOWN_SEC:
            remain = int(self.COOLDOWN_SEC - (now - last).total_seconds())
            print(f"[CD] user {uid} tp '{city}' blocked: {remain}s remaining")
            return

        item_key = TP_ITEMS.get(city)
        if not item_key:
            print(f"[INFO] unknown tp city '{city}' from user {uid} ({char_name}); ignore")
            return

        family = self.lookup_family_name_by_userno(uid)
        if not family:
            print(f"[WARN] tp {city}: cannot resolve family for userNo={uid}")
            return

        symno = self.send_mail(family, item_key, 1)
        if symno:
            self.cooldown[uid] = now  # or rescue_cd
            print(f"[OK] {ts} tp {city} -> mailed item {item_key} x1 to @{family} (symNo={symno})")
        else:
            print(f"[ERR] sendMail failed for @{family} (tp {city})")

    def lookup_family_name_by_userno(self, user_no) -> str:
        q = f"""
        SELECT TOP (1) _userNickname
        FROM {DB_WORLD}.PaGamePrivate.TblUserInformation WITH (READUNCOMMITTED)
        WHERE _userNo = ?;
        """
        try:
            cur = self.cn_world.cursor()
            cur.execute(q, user_no)
            row = cur.fetchone()
            return row[0] if row else None
        except Exception as e:
            print(f"[ERR] family lookup failed: {e}")
            return None

    def send_mail(self, to_family: str, item_key: int, count: int) -> str:
        try:
            cur = self.cn_world.cursor()
            cur.execute("""
                DECLARE @symNo NVARCHAR(50);
                EXEC dbo.sendMail
                    @toFamilyName = ?,
                    @itemKey      = ?,
                    @itemCount    = ?,
                    @enchant      = ?,
                    @title        = ?,
                    @contents     = ?,
                    @symNo        = @symNo OUTPUT;
                SELECT @symNo AS symNo;
            """, (to_family, item_key, count, MAIL_ENCHANT, MAIL_TITLE, MAIL_BODY))

            sym_no = None
            # Walk all result sets; pick the one that exposes symNo
            while True:
                if cur.description:
                    cols = [d[0].lower() for d in cur.description]
                    if "symno" in cols:
                        idx = cols.index("symno")
                        row = cur.fetchone()
                        if row:
                            sym_no = row[idx]
                    # drain remaining rows in this set
                    for _ in cur:
                        pass
                if not cur.nextset():
                    break

            return sym_no or "OK"  # treat as success even if symNo wasn’t surfaced
        except Exception as e:
            print(f"[ERR] sendMail error: {e}")
            return None

def main():
    w = ChatCommandWatcher()
    try:
        w.run()
    except KeyboardInterrupt:
        w.stop.set()
        print("Stopping…")

if __name__ == "__main__":
    main()
 
this need account exclusive to GM or any account?
 
Back