Автор не я взял с Англ сайта
Приветствую.
Я создал свой чёрный рынок. Это веб-сайт. Игрок вводит свой логин/пароль для игры.
После аутентификации они попадают на рынок для покупки предметов. Вы контролируете, что добавляется/удаляется и т.д.
Средства, используемые для покупки, поступают с Центрального рынка.
В данном случае мой ник — james.
У меня на Центральном рынке 4 519 206 984 предмета.
Я не смог добавить эссенцию духа Орнетта на свой Центральный рынок, поэтому создал свой собственный чёрный рынок. Это оказалось проще, чем я ожидал.
Игрок входит в систему, нажимает кнопку «Купить», и в игре получает письмо с приобретённым предметом. Его баланс на центральном рынке мгновенно меняется. Выходить из системы не нужно.
Для этого используются:
1. Python 3.9.5 (устанавливается при установке Corsair).
2. Базы данных (устанавливаются при установке Corsair)
. 3. Открытый порт на брандмауэре (в моём случае я использовал 8892).
4. Он блокирует строку базы данных и изменяет её, чтобы предотвратить несанкционированное использование.
5. Sendmail, который необходимо добавить в базу данных. Я также включу его.
6. HTML-код встроен в файл Python, поэтому нет необходимости создавать несколько файлов.
7. Вы можете редактировать содержимое вашего рынка и цены примерно в строках 40–50.
С Send Mail легко работать. Откройте SQL-сервер с помощью SQL Server Manager.
Войдите в систему.
Откройте папку с базами данных. Найдите
SA_BETA_WORLDDB_0002 и разверните её.
Разверните раздел «Программируемость».
Разверните раздел «Хранимые процедуры». (Вот где всё и дело).
Я использую эту процедуру sendMail каждый раз, когда хочу отправить письмо пользователю. (Жемчуг/всё такое).
Итак, вот как это сделать.
Щёлкните правой кнопкой мыши по разделу «Хранимые процедуры», наведите курсор на пункт «Создать» и выберите «Хранимая процедура». (Мы создаём новую).
Откроется шаблон. Удалите всё это. Оставьте его пустым. Вставьте это и найдите кнопку «Выполнить» в меню (вверху и слева от текущей позиции).
(или нажмите F5)
. Обновите. Убедитесь, что новая процедура называется dbo.sendMail.
Приветствую.
Я создал свой чёрный рынок. Это веб-сайт. Игрок вводит свой логин/пароль для игры.

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

Игрок входит в систему, нажимает кнопку «Купить», и в игре получает письмо с приобретённым предметом. Его баланс на центральном рынке мгновенно меняется. Выходить из системы не нужно.
Для этого используются:
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
Войдите в систему.
Откройте папку с базами данных. Найдите
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