Мануал Corsair - Custom Central/Black Market

orohimaru2

Выдающийся
Местный
Сообщения
57
Розыгрыши
0
Репутация
114
Реакции
199
Баллы
1 388
Автор не я взял с Англ сайта
Приветствую.
Я создал свой чёрный рынок. Это веб-сайт. Игрок вводит свой логин/пароль для игры.
1.webp
После аутентификации они попадают на рынок для покупки предметов. Вы контролируете, что добавляется/удаляется и т.д.
Средства, используемые для покупки, поступают с Центрального рынка.

В данном случае мой ник — james.
У меня на Центральном рынке 4 519 206 984 предмета.
Я не смог добавить эссенцию духа Орнетта на свой Центральный рынок, поэтому создал свой собственный чёрный рынок. Это оказалось проще, чем я ожидал.
2.webp
Игрок входит в систему, нажимает кнопку «Купить», и в игре получает письмо с приобретённым предметом. Его баланс на центральном рынке мгновенно меняется. Выходить из системы не нужно.

Для этого используются:
1. Python 3.9.5 (устанавливается при установке Corsair).
2. Базы данных (устанавливаются при установке Corsair)
. 3. Открытый порт на брандмауэре (в моём случае я использовал 8892).
4. Он блокирует строку базы данных и изменяет её, чтобы предотвратить несанкционированное использование.
5. Sendmail, который необходимо добавить в базу данных. Я также включу его.
6. HTML-код встроен в файл Python, поэтому нет необходимости создавать несколько файлов.
7. Вы можете редактировать содержимое вашего рынка и цены примерно в строках 40–50.
Код:
'''
    BlackMarket
        A blackmarket server for black desert.
   
    Author: Njinir

Notes:
    Login DB: SA_BETA_WORLDDB_0002 at table PaGamePrivate.TblUserInformation
    Target:
    Database: [SA_BETA_TRADEDB_0002].[PaGamePrivate].[TblWorldMarketWallet]
    Where: _userNo =
    and : _mainKey = 1
    and : _count = Silver Count for transactions

'''

# blackmarket.py
# Flask + Waitress, Python 3.9.5, ODBC Driver 17, port 8892
import os
from datetime import timedelta
import pyodbc
from flask import (
    Flask, request, redirect, url_for, session,
    render_template_string, flash
)

app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET", "dev-secret")  # change in prod
app.permanent_session_lifetime = timedelta(hours=2)

# --- DB config ---
SQL_SERVER = os.environ.get("SQL_SERVER", "localhost")
USE_SQL_AUTH = os.environ.get("USE_SQL_AUTH", "0") == "1"  # set to 1 to use SQL auth
SQL_USER = os.environ.get("SQL_USER", "")
SQL_PASS = os.environ.get("SQL_PASS", "")

WORLD_DB = "SA_BETA_WORLDDB_0002"
TRADE_DB = "SA_BETA_TRADEDB_0002"

# --- Demo items (IDs/prices are placeholders) ---
ITEMS = [
    {"id": 40770, "name": "Ornette's Spirit Essence", "price": 4_000_000_000, "enchant": 0},
    {"id": 40771, "name": "Odore's Spirit Essence", "price": 4_000_000_000, "enchant": 0},
    {"id": 16016, "name": "Laffi Bedmountains Compass", "price": 4_000_000_000, "enchant": 0},
    {"id": 16019, "name": "Archaeologist's Map", "price": 4_000_000_000, "enchant": 0},
    {"id": 50151, "name": "Dream Arduanatt", "price": 5_000_000_000, "enchant": 0},
    {"id": 50150, "name": "Dream Dine", "price": 5_000_000_000, "enchant": 0},
    {"id": 50149, "name": "Dream Doom", "price": 5_000_000_000, "enchant": 0},
    {"id": 47968, "name": "Celestial Horse Horn", "price": 1_000_000_000, "enchant": 0},
    {"id": 16080, "name": "Cron Stone x10", "price": 2_000_000, "enchant": 0},
]

def get_conn(database_name: str):
    """Create an ODBC connection to the specified database."""
    driver = "{ODBC Driver 17 for SQL Server}"
    if USE_SQL_AUTH:
        cs = (
            f"DRIVER={driver};SERVER={SQL_SERVER};DATABASE={database_name};"
            f"UID={SQL_USER};PWD={SQL_PASS};TrustServerCertificate=yes;"
        )
    else:
        cs = (
            f"DRIVER={driver};SERVER={SQL_SERVER};DATABASE={database_name};"
            f"Trusted_Connection=yes;TrustServerCertificate=yes;"
        )
    return pyodbc.connect(cs)

def get_wallet_silver(user_no: int) -> int:
    """Read silver from TRADE DB Wallet where _mainKey = 1."""
    try:
        with get_conn(TRADE_DB) as conn, conn.cursor() as cur:
            cur.execute("""
                SELECT TOP 1 _count
                FROM PaGamePrivate.TblWorldMarketWallet WITH (NOLOCK)
                WHERE _userNo = ? AND _mainKey = 1
            """, (user_no,))
            row = cur.fetchone()
            return int(row[0]) if row and row[0] is not None else 0
    except Exception:
        return 0

# ---------- HTML (inline for single-file app) ----------
LOGIN_HTML = """<!doctype html>
<html><head>
  <meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>Black Market – Sign in</title>
  <style>
    body{margin:0;font-family:system-ui,Segoe UI,Arial;background:#0b0d11;color:#e6e6e6;display:grid;place-items:center;height:100vh}
    .card{background:#141922;border:1px solid #232a36;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.35);padding:28px;min-width:300px;max-width:360px}
    h1{margin:0 0 6px 0;font-size:22px}
    p.sub{margin:0 0 16px 0;color:#aab3c2;font-size:13px}
    label{display:block;margin:12px 0 6px 0;font-size:13px;color:#aab3c2}
    input{width:100%;padding:10px 12px;border:1px solid #2a3140;background:#0f131a;color:#e6e6e6;border-radius:10px}
    button{margin-top:16px;width:100%;padding:10px 12px;border:0;border-radius:10px;background:#3b82f6;color:white;font-weight:600;cursor:pointer}
    .err{margin-top:10px;color:#ff7070;font-size:13px}
    .msg{margin-top:10px;color:#87d17e;font-size:13px}
  </style>
</head>
<body>
  <form class="card" method="post" action="/">
    <h1>Black Market</h1>
    <p class="sub">Sign in</p>
    <label>Username</label>
    <input name="username" autocomplete="username" required />
    <label>Password</label>
    <input name="password" type="password" autocomplete="current-password" required />
    <button type="submit">Sign in</button>
    {% if error %}<div class="err">{{ error }}</div>{% endif %}
    {% for m in get_flashed_messages() %}<div class="msg">{{ m }}</div>{% endfor %}
  </form>
</body></html>"""

HOME_HTML = """<!doctype html>
<html><head>
  <meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>Black Market – Home</title>
  <style>
    input.qty{width:90px;padding:6px 8px;border-radius:8px;border:1px solid #232a36;background:#0f131a;color:#e6e6e6}
    body{background:#0b0d11;color:#e6e6e6;font-family:system-ui,Segoe UI,Arial;margin:0}
    header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:#141922;border-bottom:1px solid #232a36}
    main{padding:24px}
    .meta{color:#aab3c2;font-size:14px;margin:6px 0 18px}
    table{width:100%;border-collapse:collapse;margin-top:10px}
    th,td{padding:10px;border-bottom:1px solid #232a36;text-align:left}
    button{padding:6px 10px;border:0;border-radius:8px;background:#3b82f6;color:#fff;cursor:pointer}
    .flash-ok{color:#7be18a;margin:6px 0}
    .flash-err{color:#ff7070;margin:6px 0}
  </style>
</head>
<body>
  <header>
    <div><strong>Black Market</strong></div>
    <form method="post" action="/logout"><button type="submit">Logout</button></form>
  </header>
  <main>
    <h2>Welcome, {{ username }}</h2>
    <div class="meta">Balance: <strong>{{ "{:,}".format(balance) }}</strong> silver</div>

    {% for m in get_flashed_messages(category_filter=["ok"]) %}<div class="flash-ok">{{ m }}</div>{% endfor %}
    {% for m in get_flashed_messages(category_filter=["err"]) %}<div class="flash-err">{{ m }}</div>{% endfor %}

    <table>
      <thead><tr><th>Item</th><th>Price (silver)</th><th>Enchant</th><th>Action</th></tr></thead>
      <tbody>
        {% for it in items %}
        <tr>
            <td>{{ it.name }}</td>
            <td>{{ "{:,}".format(it.price) }}</td>
            <td>{{ it.get("enchant", 0) }}</td>
            <td>
                <form method="post" action="/buy" style="display:flex;gap:8px;align-items:center">
                <input type="hidden" name="item_id" value="{{ it.id }}">
                <label for="qty-{{ it.id }}" style="font-size:12px;color:#aab3c2">Qty</label>
                <input id="qty-{{ it.id }}" class="qty" type="number" name="qty" value="1" min="1" step="1" />
                <button type="submit">Buy</button>
                </form>
            </td>
        </tr>
        {% endfor %}
      </tbody>
    </table>
  </main>
</body></html>"""

# -------------------- Routes --------------------

@app.route("/", methods=["GET", "POST"])
def login():
    error = None
    if request.method == "POST":
        username = request.form.get("username", "").strip()
        password = request.form.get("password", "")
        if not username or not password:
            error = "Enter both fields."
        else:
            user_id = f"{username},{password}"  # _userId format: "username,password"
            try:
                with get_conn(WORLD_DB) as conn, conn.cursor() as cur:
                    cur.execute("""
                        SELECT TOP 1 _userNo, _userNickname
                        FROM PaGamePrivate.TblUserInformation WITH (NOLOCK)
                        WHERE _userId = ?
                    """, (user_id,))
                    row = cur.fetchone()
                if row:
                    session.permanent = True
                    session["username"] = username
                    session["_userNo"] = int(row[0])
                    session["_nickname"] = row[1] or username  # for @toFamilyName
                    return redirect(url_for("home"))
                else:
                    error = "Invalid credentials."
            except Exception as ex:
                error = f"DB error: {ex}"
    return render_template_string(LOGIN_HTML, error=error)

@app.route("/home")
def home():
    if "username" not in session:
        return redirect(url_for("login"))
    balance = get_wallet_silver(session["_userNo"])
    return render_template_string(
        HOME_HTML,
        username=session["username"],
        balance=balance,
        items=ITEMS
    )

@app.route("/logout", methods=["POST"])
def logout():
    session.clear()
    flash("Signed out.", "ok")
    return redirect(url_for("login"))

@app.route("/buy", methods=["POST"])
def buy():
    if "username" not in session:
        return redirect(url_for("login"))

    item_id = request.form.get("item_id", "").strip()
    qty_raw = request.form.get("qty", "1").strip()
    try:
        qty = max(1, int(qty_raw))
    except ValueError:
        qty = 1

    # Lookup item from ITEMS (id == itemKey for sendMail)
    item = next((i for i in ITEMS if str(i["id"]) == item_id), None)
    if not item:
        flash("Invalid item.", "err")
        return redirect(url_for("home"))

    item_key   = int(item["id"])
    unit_price = int(item["price"])   # e.g., 2_000_000 → 2000000
    item_name  = item["name"]
    enchant    = int(item.get("enchant", 0))
    total_cost = unit_price * qty

    user_no   = session["_userNo"]
    nickname  = session.get("_nickname", session["username"])

    # Begin atomic TX: deduct wallet and send mail
    try:
        conn = get_conn(TRADE_DB)
        conn.autocommit = False
        cur = conn.cursor()

        # Lock wallet row, check balance
        cur.execute("""
            DECLARE @bal BIGINT;
            SELECT @bal = _count
            FROM PaGamePrivate.TblWorldMarketWallet WITH (UPDLOCK, ROWLOCK)
            WHERE _userNo = ? AND _mainKey = 1;

            IF @bal IS NULL
                THROW 50001, 'Wallet not found', 1;
            IF @bal < ?
                THROW 50002, 'Insufficient funds', 1;
        """, (user_no, total_cost))

        # Deduct
        cur.execute("""
            UPDATE PaGamePrivate.TblWorldMarketWallet
            SET _count = _count - ?
            WHERE _userNo = ? AND _mainKey = 1;
        """, (total_cost, user_no))

        # Send mail (fully-qualified proc name; runs in same TX)
        # @toFamilyName NVARCHAR, @itemKey INT, @itemCount INT, @enchant INT,
        # @title NVARCHAR, @contents NVARCHAR, @symNo NVARCHAR(OUTPUT)
        cur.execute("""
            DECLARE @symNo NVARCHAR(50);
            EXEC SA_BETA_WORLDDB_0002.dbo.sendMail
                @toFamilyName = ?,
                @itemKey      = ?,
                @itemCount    = ?,
                @enchant      = ?,
                @title        = N'Mail Express',
                @contents     = N'Mail sent from the administration team.',
                @symNo        = @symNo OUTPUT;

            SELECT @symNo AS symNo;
        """, (nickname, item_key, qty, enchant))

        sym_row = cur.fetchone()
        sym_no = sym_row[0] if (sym_row and sym_row[0]) else None

        conn.commit()
        cur.close(); conn.close()

        msg = f"Purchased {item_name} x{qty} for {total_cost:,} silver."
        if sym_no:
            msg += f" Mail symNo: {sym_no}"
        flash(msg, "ok")
        return redirect(url_for("home"))

    except Exception as e:
        # Roll back any partial changes
        try:
            conn.rollback()
        except Exception:
            pass
        try:
            cur.close(); conn.close()
        except Exception:
            pass

        msg = str(e)
        if "50002" in msg or "Insufficient funds" in msg:
            flash(f"Insufficient funds. Need {total_cost:,}.", "err")
        else:
            flash(f"Purchase failed: {msg}", "err")
        return redirect(url_for("home"))

# -------------------- Entry point (Waitress) --------------------
if __name__ == "__main__":
    from waitress import serve
    print("Serving at http://0.0.0.0:8892")
    serve(app, host="0.0.0.0", port=8892)  # bind only this port/interface
С Send Mail легко работать. Откройте SQL-сервер с помощью SQL Server Manager.
Войдите в систему.
Откройте папку с базами данных. Найдите
SA_BETA_WORLDDB_0002 и разверните её.
Разверните раздел «Программируемость».
Разверните раздел «Хранимые процедуры». (Вот где всё и дело).

Я использую эту процедуру sendMail каждый раз, когда хочу отправить письмо пользователю. (Жемчуг/всё такое).


Итак, вот как это сделать.
Щёлкните правой кнопкой мыши по разделу «Хранимые процедуры», наведите курсор на пункт «Создать» и выберите «Хранимая процедура». (Мы создаём новую).
Откроется шаблон. Удалите всё это. Оставьте его пустым. Вставьте это и найдите кнопку «Выполнить» в меню (вверху и слева от текущей позиции).
(или нажмите F5)

. Обновите. Убедитесь, что новая процедура называется dbo.sendMail.
Код:
USE [SA_BETA_WORLDDB_0002]
GO
/****** Object:  StoredProcedure [dbo].[sendMail]    Script Date: 8/28/2025 2:25:51 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

-- This script ALTERS the existing dbo.sendMail procedure in SA_BETA_WORLDDB_0002.
-- The key change is to use three-part naming for the cross-database call to uspSendMailByUserWithEnchant_XXX.
ALTER PROCEDURE [dbo].[sendMail]
     @toFamilyName          NVARCHAR(50)
    ,@itemKey                BIGINT
    ,@itemCount                BIGINT = 1
    ,@enchant                INT = 0
    ,@title                    NVARCHAR(100) = N'Your donation!'
    ,@contents                NVARCHAR(300) = N'Thank you for supporting us!'
    ,@symNo                    NVARCHAR(50)    OUTPUT    -- Meaningful only in case of failure
AS
BEGIN
    SET NOCOUNT ON                                        -- Do not generate count-set results.
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
    SET LOCK_TIMEOUT 3000                                -- Do not change arbitrarily; consult DBA if needed.
    SET XACT_ABORT ON

    DECLARE @rv        INT = 0,                            -- 0: Success, Negative: Error Code, Positive: Not to be used (handled as ODBC error)
            @userNo BIGINT,
            @userId NVARCHAR(50)
   
    BEGIN TRANSACTION

    -- Using SA_BETA_WORLDDB_0002 explicitly based on your clarification that CORSAIR_WORLDDB is equivalent.
    SELECT @userNo=_userNo, @userId=_userId FROM [SA_BETA_WORLDDB_0002].PaGamePrivate.TblUserInformation where _userNickname=@toFamilyName
    if @@ROWCOUNT <> 1
    begin
        set @rv = -1
        set @symNo = 'errNoFamilyDoesNotExist'
        goto LABEL_END
    end

    -- *** THE CRITICAL FIX: Explicitly specify the database for the cross-database call ***
    EXEC    @rv = [SA_BETA_GAMEDB_0002].[PaGamePublic].[uspSendMailByUserWithEnchant_XXX]
            @senderName = N'Primal Rage Gaming Team', -- Or N'RogueBD' as seen in your other script
            @senderUserNo = 1,
            @toUserNo = @userNo,
            @title = @title,
            @contents = @contents,
            @itemKey = @itemKey,
            @enchantLevel = @enchant,
            @itemCount = @itemCount

    if @rv <> 0
    begin
        set @symNo = 'errNoMailNotSent'
        GOTO LABEL_END
    end
    /*
    -- The original script had a commented-out INSERT into EVOBDO_LOGDB_0001.PaGamePrivate.TblItemLog.
    -- If you need this logging, it would also require three-part naming if EVOBDO_LOGDB_0001 is a different database.
    INSERT INTO [EVOBDO_LOGDB_0001].PaGamePrivate.TblItemLog ([_operationLogType], [_serverNo], [_registerDate], [_userId], [_userNo], [_isUserGm],
                [_isPcRoom], [_isPcRoomPremium], [_itemNo], [_itemKey], [_endurance], [_maxEndurance], [_itemWhereType], [_variedCount], [_reason], [_receivingUserId], [_receivingUserNo])
    VALUES (
        100,    -- unsure, could be 101
        0,        -- who cares
        GETDATE(),
        'dbo.sendMail',
        1,    -- userNo
        1,    -- is GM
        0, 0, -- pr room
        0,    -- itemNo
        @itemKey,
        0, 0, -- endurance
        0, -- where
        @itemCount,
        50,    -- reason: send mail
        @userId,
        @userNo
    )
    */
    if @rv <> 0
    begin
        set @symNo = 'errNoMailNotLogged'
        GOTO LABEL_END
    end

LABEL_END:
    IF(0 = @rv)
    BEGIN
        COMMIT TRAN
    END
    ELSE
    BEGIN
        ROLLBACK TRAN
    END
    RETURN(@rv)
END
 

Назад
Сверху