41 Commits

Author SHA1 Message Date
server
b4ee8aa5d7 Revert "game: escape log query string fields"
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
This reverts commit f4f2692ce3.
2026-04-14 16:45:11 +02:00
server
b54161699f Revert "game: make log escaping safe before SQL connect"
This reverts commit 52278ce6f2.
2026-04-14 16:45:11 +02:00
server
52278ce6f2 game: make log escaping safe before SQL connect
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 16:43:52 +02:00
server
f4f2692ce3 game: escape log query string fields
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 16:41:45 +02:00
server
646e40eaf8 runtime: log pid lifecycle at info level
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 16:39:25 +02:00
server
6d24172c51 db: treat empty table postfix as expected default
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 16:37:34 +02:00
server
e56d93ad53 libsql: avoid redundant threaded locale resets
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 16:35:43 +02:00
server
e306c7c500 game: suppress repeated close-phase send noise
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 16:30:08 +02:00
server
f702a64305 db: clarify peer disconnect diagnostics
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 16:23:33 +02:00
server
bba87f939f db: prepare item award manager queries
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 14:48:05 +02:00
server
44f9d92e8c tests: cover quest framing and restart cooldowns
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 14:38:21 +02:00
server
cf6deb1895 game: fix post-restart mall open flow
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 14:27:15 +02:00
server
bc1175eb35 tests: cover headless character delete
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 12:56:46 +02:00
server
5f11a4fef0 db: prepare player delete cleanup flow
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 12:48:48 +02:00
server
d85fa6c12c db: prepare player create flow
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 12:42:05 +02:00
server
6b274186c5 tests: exercise mall open in smoke login
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 12:33:08 +02:00
server
84ed35cbda db: prepare safebox load bootstrap
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 11:47:33 +02:00
server
aa862d829d db: log SQL worker shutdown state
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 11:22:30 +02:00
server
719440575f db: prepare safebox password change flow
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 11:04:06 +02:00
server
c5cac17125 game: prepare auth login query
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 10:58:11 +02:00
server
f1e64196ae db: fix login-by-key peer handle usage 2026-04-14 10:58:05 +02:00
server
5695612b37 tests: add login smoke json and failure modes
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 10:45:15 +02:00
server
ef9324025e tests: extend login smoke to entergame
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 10:30:25 +02:00
server
27c5742c11 db: prepare login-by-key player index bootstrap
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 10:11:11 +02:00
server
58273bc116 db: prepare guild and rename queries
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 09:58:34 +02:00
server
14e084d691 config: support env-based SQL overrides
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 09:41:31 +02:00
server
b46a2661df docs: add repo instructions
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 09:09:32 +02:00
server
e99b8b0520 tests: support env-backed smoke passwords
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 08:49:30 +02:00
server
b2b037fb94 tests: add end-to-end login smoke utility
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 08:42:05 +02:00
server
e2c43240ec net: add Linux epoll fdwatch backend
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:57:02 +02:00
server
e638816026 runtime: replace virtual checkpoint timer with watchdog
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:40:07 +02:00
server
cecc822777 runtime: encapsulate checkpoint progress state
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:12:27 +02:00
server
3272537376 net: speed up select fdwatch lookups
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:08:23 +02:00
server
4f1e7b0e9c net: encapsulate fdwatch backend metadata
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 07:03:44 +02:00
server
abed660256 runtime: fix clean shutdown exit codes
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 06:26:37 +02:00
server
7daf51f2da runtime: improve startup observability
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 06:04:12 +02:00
server
3e3f0918e9 tests: add socket auth smoke flow 2026-04-14 06:04:08 +02:00
server
25ec562ab0 config: harden admin page password handling
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled
2026-04-14 01:07:31 +02:00
server
c8146c0340 ci: add Linux smoke coverage 2026-04-14 01:07:27 +02:00
server
e8a33f84f4 net: fix Linux select fdwatch readiness
Some checks failed
build / Main build job (push) Has been cancelled
2026-04-14 00:42:45 +02:00
server
1ca64ce829 net: flush queued packets immediately 2026-04-14 00:31:50 +02:00
45 changed files with 5069 additions and 1364 deletions

View File

@@ -4,11 +4,43 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
bsd:
linux:
runs-on: ubuntu-latest
name: Main build job
strategy:
fail-fast: false
matrix:
include:
- name: release
build_type: Release
enable_asan: OFF
- name: asan
build_type: RelWithDebInfo
enable_asan: ON
name: Linux ${{ matrix.name }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build pkg-config libsodium-dev libmariadb-dev
- name: Configure
run: |
cmake -S . -B build-${{ matrix.name }} -G Ninja \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DENABLE_ASAN=${{ matrix.enable_asan }}
- name: Build
run: cmake --build build-${{ matrix.name }} --parallel
- name: Smoke tests
run: ctest --test-dir build-${{ matrix.name }} --output-on-failure
freebsd:
runs-on: ubuntu-latest
name: FreeBSD build
steps:
- uses: actions/checkout@v4
- name: FreeBSD job
@@ -24,6 +56,7 @@ jobs:
cd build
cmake ..
gmake all -j6
ctest --output-on-failure
- name: Collect outputs
run: |
mkdir _output
@@ -32,4 +65,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: output_bsd
path: _output
path: _output

67
AGENTS.md Normal file
View File

@@ -0,0 +1,67 @@
# AGENTS.md
## Repository Role
This repository contains the C++ server source and build system for:
- `game`
- `db`
- `qc`
- smoke/login test binaries
The current production VPS runs a Debian deployment built from `main`.
## Working Rules
- Prefer small, reviewable changes directly on `main`.
- Keep Debian runtime stability ahead of broad refactors.
- Do not commit `build/`, temporary captures, or host-local debug output.
- Do not commit secrets or production-only credentials.
## Build And Test
Typical local build:
```bash
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
```
Important test targets:
- `metin_smoke_tests`
- `metin_login_smoke`
## High-Risk Areas
Changes in these areas require extra care:
- auth/login flow
- packet headers and packet structs
- `SecureCipher`
- `fdwatch` and socket output path
- DB login/account flow
When changing login, auth, packet, or network behavior:
- build and run smoke tests
- keep protocol compatibility with `m2dev-client-src`
- keep client asset behavior in sync with `m2dev-client`
## Production Verification
The current VPS has a root-only end-to-end login check:
```bash
/usr/local/sbin/metin-login-healthcheck
```
Use it after risky auth/network/runtime changes.
## Cross-Repo Boundaries
- server code changes belong here
- runtime/config/systemd/docs changes belong in `m2dev-server`
- client protocol implementation changes belong in `m2dev-client-src`
- client asset/login screen/server list changes belong in `m2dev-client`

11
CLAUDE.md Normal file
View File

@@ -0,0 +1,11 @@
# CLAUDE.md
Follow [AGENTS.md](AGENTS.md) as the canonical repo guide.
Short version:
- this repo owns `game`, `db`, `qc`, and smoke/login test binaries
- prefer small changes on `main`
- auth/network/packet edits must be tested carefully
- keep protocol changes aligned with `m2dev-client-src` and `m2dev-client`
- use `/usr/local/sbin/metin-login-healthcheck` on the VPS after risky login/runtime changes

View File

@@ -4,6 +4,7 @@ project(m2dev-server-src)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ASan support
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
@@ -98,3 +99,6 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/vendor/mariadb-connector-c-3.4.5
add_subdirectory(src)
add_subdirectory(vendor)
enable_testing()
add_subdirectory(tests)

View File

@@ -17,6 +17,7 @@
#include "Marriage.h"
#include "ItemIDRangeManager.h"
#include "Cache.h"
#include "libsql/Statement.h"
#include <memory>
@@ -37,6 +38,167 @@ CPacketInfo g_item_info;
int g_item_count = 0;
int g_query_count[2];
namespace
{
bool PrepareClientPlayerStmt(CStmt& stmt, const char* query)
{
CAsyncSQL* sql = CDBManager::instance().GetDirectSQL(SQL_PLAYER);
if (!sql)
{
sys_err("player SQL handle is not initialized");
return false;
}
return stmt.Prepare(sql, query);
}
bool LoadSafeboxPasswordByAccountId(DWORD accountId, char* password, size_t passwordSize, bool* found)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "SELECT password FROM safebox%s WHERE account_id = ?", GetTablePostfix());
*found = false;
password[0] = '\0';
CStmt stmt;
if (!PrepareClientPlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &accountId))
return false;
if (!stmt.BindResult(MYSQL_TYPE_STRING, password, passwordSize))
return false;
if (!stmt.Execute())
return false;
if (stmt.iRows == 0)
return true;
if (!stmt.Fetch())
return false;
*found = true;
return true;
}
bool UpdateSafeboxPasswordByAccountId(DWORD accountId, const char* password)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "UPDATE safebox%s SET password = ? WHERE account_id = ?", GetTablePostfix());
CStmt stmt;
if (!PrepareClientPlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, const_cast<char*>(password), SAFEBOX_PASSWORD_MAX_LEN))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &accountId))
return false;
return stmt.Execute() != 0;
}
bool MatchesSafeboxPassword(const char* storedPassword, const char* providedPassword)
{
if ((storedPassword && *storedPassword))
return !strcasecmp(storedPassword, providedPassword);
return !strcmp("000000", providedPassword);
}
bool LoadSafeboxTableByAccountId(DWORD accountId, const char* providedPassword, bool isMall, TSafeboxTable* safebox, bool* wrongPassword)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "SELECT account_id, size, password FROM safebox%s WHERE account_id = ?", GetTablePostfix());
DWORD loadedAccountId = 0;
DWORD loadedSize = 0;
char storedPassword[SAFEBOX_PASSWORD_MAX_LEN + 1] = {};
CStmt stmt;
if (!PrepareClientPlayerStmt(stmt, query))
return false;
*wrongPassword = false;
memset(safebox, 0, sizeof(TSafeboxTable));
if (!stmt.BindParam(MYSQL_TYPE_LONG, &accountId))
return false;
if (!stmt.BindResult(MYSQL_TYPE_LONG, &loadedAccountId))
return false;
if (!stmt.BindResult(MYSQL_TYPE_LONG, &loadedSize))
return false;
if (!stmt.BindResult(MYSQL_TYPE_STRING, storedPassword, sizeof(storedPassword)))
return false;
if (!stmt.Execute())
return false;
if (stmt.iRows == 0)
{
safebox->dwID = accountId;
*wrongPassword = strcmp("000000", providedPassword) != 0;
return true;
}
if (!stmt.Fetch())
return false;
if (((!storedPassword[0]) && strcmp("000000", providedPassword))
|| (storedPassword[0] && strcmp(storedPassword, providedPassword)))
{
*wrongPassword = true;
return true;
}
safebox->dwID = loadedAccountId == 0 ? accountId : loadedAccountId;
safebox->bSize = static_cast<BYTE>(loadedSize);
if (isMall)
{
safebox->bSize = 1;
sys_log(0, "MALL id[%u] size[%u]", safebox->dwID, safebox->bSize);
}
else
{
sys_log(0, "SAFEBOX id[%u] size[%u]", safebox->dwID, safebox->bSize);
}
return true;
}
void QueueSafeboxItemsLoad(CPeer* peer, CClientManager::ClientHandleInfo* info)
{
char query[512];
snprintf(query, sizeof(query),
"SELECT id, window+0, pos, count, vnum, socket0, socket1, socket2, "
"attrtype0, attrvalue0, "
"attrtype1, attrvalue1, "
"attrtype2, attrvalue2, "
"attrtype3, attrvalue3, "
"attrtype4, attrvalue4, "
"attrtype5, attrvalue5, "
"attrtype6, attrvalue6 "
"FROM item%s WHERE owner_id=%u AND window='%s'",
GetTablePostfix(), info->account_id, info->ip[0] == 0 ? "SAFEBOX" : "MALL");
CDBManager::instance().ReturnQuery(query, QID_SAFEBOX_LOAD, peer->GetHandle(), info);
}
void EncodeSafeboxPasswordChangeAnswer(CPeer* pkPeer, DWORD dwHandle, BYTE success)
{
pkPeer->EncodeHeader(DG::SAFEBOX_CHANGE_PASSWORD_ANSWER, dwHandle, sizeof(BYTE));
pkPeer->EncodeBYTE(success);
}
}
CClientManager::CClientManager() :
m_pkAuthPeer(NULL),
m_iPlayerIDStart(0),
@@ -450,16 +612,30 @@ void CClientManager::QUERY_SAFEBOX_LOAD(CPeer * pkPeer, DWORD dwHandle, TSafebox
pi->account_index = 0;
pi->ip[0] = bMall ? 1 : 0;
strlcpy(pi->login, packet->szLogin, sizeof(pi->login));
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery),
"SELECT account_id, size, password FROM safebox%s WHERE account_id=%u",
GetTablePostfix(), packet->dwID);
if (g_log)
sys_log(0, "GD::SAFEBOX_LOAD (handle: %d account.id %u is_mall %d)", dwHandle, packet->dwID, bMall ? 1 : 0);
CDBManager::instance().ReturnQuery(szQuery, QID_SAFEBOX_LOAD, pkPeer->GetHandle(), pi);
TSafeboxTable* safebox = new TSafeboxTable;
bool wrongPassword = false;
if (!LoadSafeboxTableByAccountId(packet->dwID, pi->safebox_password, bMall, safebox, &wrongPassword))
{
delete safebox;
delete pi;
return;
}
if (wrongPassword)
{
delete safebox;
delete pi;
pkPeer->EncodeHeader(DG::SAFEBOX_WRONG_PASSWORD, dwHandle, 0);
return;
}
pi->pSafebox = safebox;
QueueSafeboxItemsLoad(pkPeer, pi);
}
void CClientManager::RESULT_SAFEBOX_LOAD(CPeer * pkPeer, SQLMsg * msg)
@@ -467,168 +643,79 @@ void CClientManager::RESULT_SAFEBOX_LOAD(CPeer * pkPeer, SQLMsg * msg)
CQueryInfo * qi = (CQueryInfo *) msg->pvUserData;
ClientHandleInfo * pi = (ClientHandleInfo *) qi->pvData;
DWORD dwHandle = pi->dwHandle;
// 여기에서 사용하는 account_index는 쿼리 순서를 말한다.
// 첫번째 패스워드 알아내기 위해 하는 쿼리가 0
// 두번째 실제 데이터를 얻어놓는 쿼리가 1
if (pi->account_index == 0)
if (!pi->pSafebox)
{
char szSafeboxPassword[SAFEBOX_PASSWORD_MAX_LEN + 1];
strlcpy(szSafeboxPassword, pi->safebox_password, sizeof(szSafeboxPassword));
TSafeboxTable * pSafebox = new TSafeboxTable;
memset(pSafebox, 0, sizeof(TSafeboxTable));
SQLResult * res = msg->Get();
if (res->uiNumRows == 0)
{
if (strcmp("000000", szSafeboxPassword))
{
pkPeer->EncodeHeader(DG::SAFEBOX_WRONG_PASSWORD, dwHandle, 0);
delete pSafebox;
delete pi;
return;
}
}
else
{
MYSQL_ROW row = mysql_fetch_row(res->pSQLResult);
// 비밀번호가 틀리면..
if (((!row[2] || !*row[2]) && strcmp("000000", szSafeboxPassword)) ||
((row[2] && *row[2]) && strcmp(row[2], szSafeboxPassword)))
{
pkPeer->EncodeHeader(DG::SAFEBOX_WRONG_PASSWORD, dwHandle, 0);
delete pSafebox;
delete pi;
return;
}
if (!row[0])
pSafebox->dwID = 0;
else
str_to_number(pSafebox->dwID, row[0]);
if (!row[1])
pSafebox->bSize = 0;
else
str_to_number(pSafebox->bSize, row[1]);
/*
if (!row[3])
pSafebox->dwGold = 0;
else
pSafebox->dwGold = atoi(row[3]);
*/
if (pi->ip[0] == 1)
{
pSafebox->bSize = 1;
sys_log(0, "MALL id[%d] size[%d]", pSafebox->dwID, pSafebox->bSize);
}
else
sys_log(0, "SAFEBOX id[%d] size[%d]", pSafebox->dwID, pSafebox->bSize);
}
if (0 == pSafebox->dwID)
pSafebox->dwID = pi->account_id;
pi->pSafebox = pSafebox;
char szQuery[512];
snprintf(szQuery, sizeof(szQuery),
"SELECT id, window+0, pos, count, vnum, socket0, socket1, socket2, "
"attrtype0, attrvalue0, "
"attrtype1, attrvalue1, "
"attrtype2, attrvalue2, "
"attrtype3, attrvalue3, "
"attrtype4, attrvalue4, "
"attrtype5, attrvalue5, "
"attrtype6, attrvalue6 "
"FROM item%s WHERE owner_id=%d AND window='%s'",
GetTablePostfix(), pi->account_id, pi->ip[0] == 0 ? "SAFEBOX" : "MALL");
pi->account_index = 1;
CDBManager::instance().ReturnQuery(szQuery, QID_SAFEBOX_LOAD, pkPeer->GetHandle(), pi);
sys_err("null safebox pointer!");
delete pi;
return;
}
else
// 쿼리에 에러가 있었으므로 응답할 경우 창고가 비어있는 것 처럼
// 보이기 때문에 창고가 아얘 안열리는게 나음
if (!msg->Get()->pSQLResult)
{
sys_err("null safebox result");
delete pi;
return;
}
static std::vector<TPlayerItem> s_items;
CreateItemTableFromRes(msg->Get()->pSQLResult, &s_items, pi->account_id);
std::set<TItemAward *> * pSet = ItemAwardManager::instance().GetByLogin(pi->login);
if (pSet && !m_vec_itemTable.empty())
{
if (!pi->pSafebox)
CGrid grid(5, MAX(1, pi->pSafebox->bSize) * 9);
bool bEscape = false;
for (DWORD i = 0; i < s_items.size(); ++i)
{
sys_err("null safebox pointer!");
delete pi;
return;
}
TPlayerItem & r = s_items[i];
itertype(m_map_itemTableByVnum) it = m_map_itemTableByVnum.find(r.vnum);
// 쿼리에 에러가 있었으므로 응답할 경우 창고가 비어있는 것 처럼
// 보이기 때문에 창고가 아얘 안열리는게 나음
if (!msg->Get()->pSQLResult)
{
sys_err("null safebox result");
delete pi;
return;
}
static std::vector<TPlayerItem> s_items;
CreateItemTableFromRes(msg->Get()->pSQLResult, &s_items, pi->account_id);
std::set<TItemAward *> * pSet = ItemAwardManager::instance().GetByLogin(pi->login);
if (pSet && !m_vec_itemTable.empty())
{
CGrid grid(5, MAX(1, pi->pSafebox->bSize) * 9);
bool bEscape = false;
for (DWORD i = 0; i < s_items.size(); ++i)
if (it == m_map_itemTableByVnum.end())
{
TPlayerItem & r = s_items[i];
bEscape = true;
sys_err("invalid item vnum %u in safebox: login %s", r.vnum, pi->login);
break;
}
itertype(m_map_itemTableByVnum) it = m_map_itemTableByVnum.find(r.vnum);
grid.Put(r.pos, 1, it->second->bSize);
}
if (!bEscape)
{
std::vector<std::pair<DWORD, DWORD> > vec_dwFinishedAwardID;
__typeof(pSet->begin()) it = pSet->begin();
char szQuery[512];
while (it != pSet->end())
{
TItemAward * pItemAward = *(it++);
const DWORD& dwItemVnum = pItemAward->dwVnum;
if (pItemAward->bTaken)
continue;
if (pi->ip[0] == 0 && pItemAward->bMall)
continue;
if (pi->ip[0] == 1 && !pItemAward->bMall)
continue;
itertype(m_map_itemTableByVnum) it = m_map_itemTableByVnum.find(pItemAward->dwVnum);
if (it == m_map_itemTableByVnum.end())
{
bEscape = true;
sys_err("invalid item vnum %u in safebox: login %s", r.vnum, pi->login);
break;
sys_err("invalid item vnum %u in item_award: login %s", pItemAward->dwVnum, pi->login);
continue;
}
grid.Put(r.pos, 1, it->second->bSize);
}
if (!bEscape)
{
std::vector<std::pair<DWORD, DWORD> > vec_dwFinishedAwardID;
__typeof(pSet->begin()) it = pSet->begin();
char szQuery[512];
while (it != pSet->end())
{
TItemAward * pItemAward = *(it++);
const DWORD& dwItemVnum = pItemAward->dwVnum;
if (pItemAward->bTaken)
continue;
if (pi->ip[0] == 0 && pItemAward->bMall)
continue;
if (pi->ip[0] == 1 && !pItemAward->bMall)
continue;
itertype(m_map_itemTableByVnum) it = m_map_itemTableByVnum.find(pItemAward->dwVnum);
if (it == m_map_itemTableByVnum.end())
{
sys_err("invalid item vnum %u in item_award: login %s", pItemAward->dwVnum, pi->login);
continue;
}
TItemTable * pItemTable = it->second;
int iPos;
@@ -766,22 +853,21 @@ void CClientManager::RESULT_SAFEBOX_LOAD(CPeer * pkPeer, SQLMsg * msg)
grid.Put(iPos, 1, it->second->bSize);
}
for (DWORD i = 0; i < vec_dwFinishedAwardID.size(); ++i)
ItemAwardManager::instance().Taken(vec_dwFinishedAwardID[i].first, vec_dwFinishedAwardID[i].second);
}
for (DWORD i = 0; i < vec_dwFinishedAwardID.size(); ++i)
ItemAwardManager::instance().Taken(vec_dwFinishedAwardID[i].first, vec_dwFinishedAwardID[i].second);
}
pi->pSafebox->wItemCount = s_items.size();
pkPeer->EncodeHeader(pi->ip[0] == 0 ? DG::SAFEBOX_LOAD : DG::MALL_LOAD, dwHandle, sizeof(TSafeboxTable) + sizeof(TPlayerItem) * s_items.size());
pkPeer->Encode(pi->pSafebox, sizeof(TSafeboxTable));
if (!s_items.empty())
pkPeer->Encode(&s_items[0], sizeof(TPlayerItem) * s_items.size());
delete pi;
}
pi->pSafebox->wItemCount = s_items.size();
pkPeer->EncodeHeader(pi->ip[0] == 0 ? DG::SAFEBOX_LOAD : DG::MALL_LOAD, dwHandle, sizeof(TSafeboxTable) + sizeof(TPlayerItem) * s_items.size());
pkPeer->Encode(pi->pSafebox, sizeof(TSafeboxTable));
if (!s_items.empty())
pkPeer->Encode(&s_items[0], sizeof(TPlayerItem) * s_items.size());
delete pi;
}
void CClientManager::QUERY_SAFEBOX_CHANGE_SIZE(CPeer * pkPeer, DWORD dwHandle, TSafeboxChangeSizePacket * p)
@@ -817,15 +903,28 @@ void CClientManager::RESULT_SAFEBOX_CHANGE_SIZE(CPeer * pkPeer, SQLMsg * msg)
void CClientManager::QUERY_SAFEBOX_CHANGE_PASSWORD(CPeer * pkPeer, DWORD dwHandle, TSafeboxChangePasswordPacket * p)
{
ClientHandleInfo * pi = new ClientHandleInfo(dwHandle);
strlcpy(pi->safebox_password, p->szNewPassword, sizeof(pi->safebox_password));
strlcpy(pi->login, p->szOldPassword, sizeof(pi->login));
pi->account_id = p->dwID;
char storedPassword[SAFEBOX_PASSWORD_MAX_LEN + 1];
bool found = false;
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery), "SELECT password FROM safebox%s WHERE account_id=%u", GetTablePostfix(), p->dwID);
if (!LoadSafeboxPasswordByAccountId(p->dwID, storedPassword, sizeof(storedPassword), &found))
{
EncodeSafeboxPasswordChangeAnswer(pkPeer, dwHandle, 0);
return;
}
CDBManager::instance().ReturnQuery(szQuery, QID_SAFEBOX_CHANGE_PASSWORD, pkPeer->GetHandle(), pi);
if (!found || !MatchesSafeboxPassword(storedPassword, p->szOldPassword))
{
EncodeSafeboxPasswordChangeAnswer(pkPeer, dwHandle, 0);
return;
}
if (!UpdateSafeboxPasswordByAccountId(p->dwID, p->szNewPassword))
{
EncodeSafeboxPasswordChangeAnswer(pkPeer, dwHandle, 0);
return;
}
EncodeSafeboxPasswordChangeAnswer(pkPeer, dwHandle, 1);
}
void CClientManager::RESULT_SAFEBOX_CHANGE_PASSWORD(CPeer * pkPeer, SQLMsg * msg)
@@ -2401,7 +2500,8 @@ void CClientManager::ProcessPackets(CPeer * peer)
break;
default:
sys_err("Unknown header (header: %d handle: %d length: %d)", header, dwHandle, dwLength);
sys_err("Unknown header (header=%u packet_handle=%u length=%u peer_handle=%u host=%s recv=%d processed=%d)",
header, dwHandle, dwLength, peer->GetHandle(), peer->GetHost(), peer->GetRecvLength(), i);
break;
}
}
@@ -2561,20 +2661,10 @@ int CClientManager::AnalyzeQueryResult(SQLMsg * msg)
case QID_ITEM_AWARD_TAKEN:
break;
// PLAYER_INDEX_CREATE_BUG_FIX
case QID_PLAYER_INDEX_CREATE:
RESULT_PLAYER_INDEX_CREATE(peer, msg);
break;
// END_PLAYER_INDEX_CREATE_BUG_FIX
case QID_PLAYER_DELETE:
__RESULT_PLAYER_DELETE(peer, msg);
break;
case QID_LOGIN_BY_KEY:
RESULT_LOGIN_BY_KEY(peer, msg);
break;
// MYSHOP_PRICE_LIST
case QID_ITEMPRICE_LOAD:
RESULT_PRICELIST_LOAD(peer, msg);
@@ -2797,18 +2887,25 @@ int CClientManager::Process()
switch (fdwatch_check_event(m_fdWatcher, peer->GetFd(), idx))
{
case FDW_READ:
if (peer->Recv() < 0)
switch (peer->Recv())
{
sys_err("Recv failed");
RemovePeer(peer);
}
else
{
if (peer == m_pkAuthPeer)
if (g_log)
sys_log(0, "AUTH_PEER_READ: size %d", peer->GetRecvLength());
case -2:
sys_log(0, "Peer disconnected cleanly. (host=%s peer_handle=%u fd=%d)", peer->GetHost(), peer->GetHandle(), peer->GetFd());
RemovePeer(peer);
break;
ProcessPackets(peer);
case -1:
sys_err("Recv failed (host=%s peer_handle=%u fd=%d)", peer->GetHost(), peer->GetHandle(), peer->GetFd());
RemovePeer(peer);
break;
default:
if (peer == m_pkAuthPeer)
if (g_log)
sys_log(0, "AUTH_PEER_READ: size %d", peer->GetRecvLength());
ProcessPackets(peer);
break;
}
break;

View File

@@ -244,7 +244,6 @@ class CClientManager : public CNetBase, public singleton<CClientManager>
void RESULT_AFFECT_LOAD(CPeer * pkPeer, MYSQL_RES * pRes, DWORD dwHandle);
// PLAYER_INDEX_CREATE_BUG_FIX
void RESULT_PLAYER_INDEX_CREATE(CPeer *pkPeer, SQLMsg *msg);
// END_PLAYER_INDEX_CREATE_BUG_FIX
// MYSHOP_PRICE_LIST
@@ -326,7 +325,6 @@ class CClientManager : public CNetBase, public singleton<CClientManager>
void QUERY_AUTH_LOGIN(CPeer * pkPeer, DWORD dwHandle, TPacketGDAuthLogin * p);
void QUERY_LOGIN_BY_KEY(CPeer * pkPeer, DWORD dwHandle, TPacketGDLoginByKey * p);
void RESULT_LOGIN_BY_KEY(CPeer * peer, SQLMsg * msg);
void ChargeCash(const TRequestChargeCash * p);

View File

@@ -7,12 +7,120 @@
#include "Config.h"
#include "QID.h"
#include "Cache.h"
#include "libsql/Statement.h"
extern std::string g_stLocale;
extern bool CreatePlayerTableFromRes(MYSQL_RES * res, TPlayerTable * pkTab);
extern int g_test_server;
extern int g_log;
namespace
{
bool LoadPlayerIndexByAccountId(DWORD account_id, TAccountTable* account_table, bool* found)
{
*found = false;
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "SELECT pid1, pid2, pid3, pid4, empire FROM player_index%s WHERE id = ?", GetTablePostfix());
CStmt stmt;
unsigned int player_ids[PLAYER_PER_ACCOUNT] = {};
unsigned int empire = 0;
if (!stmt.Prepare(CDBManager::instance().GetDirectSQL(SQL_PLAYER), query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &account_id))
return false;
for (int i = 0; i < PLAYER_PER_ACCOUNT; ++i)
{
if (!stmt.BindResult(MYSQL_TYPE_LONG, &player_ids[i]))
return false;
}
if (!stmt.BindResult(MYSQL_TYPE_LONG, &empire))
return false;
if (!stmt.Execute())
return false;
if (stmt.iRows == 0)
return true;
if (!stmt.Fetch())
return false;
for (int i = 0; i < PLAYER_PER_ACCOUNT; ++i)
account_table->players[i].dwID = player_ids[i];
account_table->bEmpire = static_cast<BYTE>(empire);
*found = true;
return true;
}
bool CreatePlayerIndexForAccount(DWORD account_id)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "INSERT IGNORE INTO player_index%s (id) VALUES(?)", GetTablePostfix());
CStmt stmt;
if (!stmt.Prepare(CDBManager::instance().GetDirectSQL(SQL_PLAYER), query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &account_id))
return false;
return stmt.Execute() != 0;
}
bool CountPlayersByNameExcludingId(const char* player_name, DWORD player_id, unsigned long long* count)
{
char query[QUERY_MAX_LEN];
if (g_stLocale == "sjis")
snprintf(query, sizeof(query), "SELECT COUNT(*) FROM player%s WHERE name = ? COLLATE sjis_japanese_ci AND id <> ?", GetTablePostfix());
else
snprintf(query, sizeof(query), "SELECT COUNT(*) FROM player%s WHERE name = ? AND id <> ?", GetTablePostfix());
CStmt stmt;
if (!stmt.Prepare(CDBManager::instance().GetDirectSQL(SQL_PLAYER), query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, (void*) player_name, CHARACTER_NAME_MAX_LEN))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &player_id))
return false;
if (!stmt.BindResult(MYSQL_TYPE_LONGLONG, count))
return false;
if (!stmt.Execute() || !stmt.Fetch())
return false;
return true;
}
bool UpdatePlayerName(DWORD player_id, const char* player_name)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "UPDATE player%s SET name = ?, change_name = 0 WHERE id = ?", GetTablePostfix());
CStmt stmt;
if (!stmt.Prepare(CDBManager::instance().GetDirectSQL(SQL_PLAYER), query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, (void*) player_name, CHARACTER_NAME_MAX_LEN))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &player_id))
return false;
return stmt.Execute() != 0;
}
}
bool CClientManager::InsertLogonAccount(const char * c_pszLogin, DWORD dwHandle, const char * c_pszIP)
{
char szLogin[LOGIN_MAX_LEN + 1];
@@ -124,55 +232,31 @@ void CClientManager::QUERY_LOGIN_BY_KEY(CPeer * pkPeer, DWORD dwHandle, TPacketG
strlcpy(info->ip, p->szIP, sizeof(info->ip));
sys_log(0, "LOGIN_BY_KEY success %s %lu %s", r.login, p->dwLoginKey, info->ip);
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery), "SELECT pid1, pid2, pid3, pid4, empire FROM player_index%s WHERE id=%u", GetTablePostfix(), r.id);
CDBManager::instance().ReturnQuery(szQuery, QID_LOGIN_BY_KEY, pkPeer->GetHandle(), info);
}
void CClientManager::RESULT_LOGIN_BY_KEY(CPeer * peer, SQLMsg * msg)
{
CQueryInfo * qi = (CQueryInfo *) msg->pvUserData;
ClientHandleInfo * info = (ClientHandleInfo *) qi->pvData;
if (msg->uiSQLErrno != 0)
bool found = false;
if (!LoadPlayerIndexByAccountId(info->pAccountTable->id, info->pAccountTable, &found))
{
peer->EncodeReturn(DG::LOGIN_NOT_EXIST, info->dwHandle);
pkPeer->EncodeReturn(DG::LOGIN_NOT_EXIST, info->dwHandle);
delete info->pAccountTable;
delete info;
return;
}
char szQuery[QUERY_MAX_LEN];
if (msg->Get()->uiNumRows == 0)
if (!found)
{
DWORD account_id = info->pAccountTable->id;
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery), "SELECT pid1, pid2, pid3, pid4, empire FROM player_index%s WHERE id=%u", GetTablePostfix(), account_id);
auto pMsg = CDBManager::instance().DirectQuery(szQuery, SQL_PLAYER);
sys_log(0, "RESULT_LOGIN_BY_KEY FAIL player_index's NULL : ID:%d", account_id);
sys_log(0, "RESULT_LOGIN_BY_KEY FAIL player_index's NULL : ID:%d", info->pAccountTable->id);
if (pMsg->Get()->uiNumRows == 0)
if (!CreatePlayerIndexForAccount(info->pAccountTable->id) ||
!LoadPlayerIndexByAccountId(info->pAccountTable->id, info->pAccountTable, &found) ||
!found)
{
sys_log(0, "RESULT_LOGIN_BY_KEY FAIL player_index's NULL : ID:%d", account_id);
// PLAYER_INDEX_CREATE_BUG_FIX
//snprintf(szQuery, sizeof(szQuery), "INSERT IGNORE INTO player_index%s (id) VALUES(%lu)", GetTablePostfix(), info->pAccountTable->id);
snprintf(szQuery, sizeof(szQuery), "INSERT INTO player_index%s (id) VALUES(%u)", GetTablePostfix(), info->pAccountTable->id);
CDBManager::instance().ReturnQuery(szQuery, QID_PLAYER_INDEX_CREATE, peer->GetHandle(), info);
// END_PLAYER_INDEX_CREATE_BUF_FIX
pkPeer->EncodeReturn(DG::LOGIN_NOT_EXIST, info->dwHandle);
delete info->pAccountTable;
delete info;
return;
}
return;
}
MYSQL_ROW row = mysql_fetch_row(msg->Get()->pSQLResult);
int col = 0;
for (; col < PLAYER_PER_ACCOUNT; ++col)
str_to_number(info->pAccountTable->players[col].dwID, row[col]);
str_to_number(info->pAccountTable->bEmpire, row[col++]);
char szQuery[QUERY_MAX_LEN];
info->account_index = 1;
extern std::string g_stLocale;
@@ -189,22 +273,9 @@ void CClientManager::RESULT_LOGIN_BY_KEY(CPeer * peer, SQLMsg * msg)
GetTablePostfix(), info->pAccountTable->id);
}
CDBManager::instance().ReturnQuery(szQuery, QID_LOGIN, peer->GetHandle(), info);
CDBManager::instance().ReturnQuery(szQuery, QID_LOGIN, pkPeer->GetHandle(), info);
}
// PLAYER_INDEX_CREATE_BUG_FIX
void CClientManager::RESULT_PLAYER_INDEX_CREATE(CPeer * pkPeer, SQLMsg * msg)
{
CQueryInfo * qi = (CQueryInfo *) msg->pvUserData;
ClientHandleInfo * info = (ClientHandleInfo *) qi->pvData;
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery), "SELECT pid1, pid2, pid3, pid4, empire FROM player_index%s WHERE id=%u", GetTablePostfix(),
info->pAccountTable->id);
CDBManager::instance().ReturnQuery(szQuery, QID_LOGIN_BY_KEY, pkPeer->GetHandle(), info);
}
// END_PLAYER_INDEX_CREATE_BUG_FIX
TAccountTable * CreateAccountTableFromRes(MYSQL_RES * res)
{
char input_pwd[PASSWD_MAX_LEN + 1];
@@ -464,44 +535,24 @@ void CClientManager::QUERY_LOGOUT(CPeer * peer, DWORD dwHandle,const char * data
void CClientManager::QUERY_CHANGE_NAME(CPeer * peer, DWORD dwHandle, TPacketGDChangeName * p)
{
char queryStr[QUERY_MAX_LEN];
if (g_stLocale == "sjis")
snprintf(queryStr, sizeof(queryStr),
"SELECT COUNT(*) as count FROM player%s WHERE name='%s' collate sjis_japanese_ci AND id <> %u",
GetTablePostfix(), p->name, p->pid);
else
snprintf(queryStr, sizeof(queryStr),
"SELECT COUNT(*) as count FROM player%s WHERE name='%s' AND id <> %u", GetTablePostfix(), p->name, p->pid);
auto pMsg = CDBManager::instance().DirectQuery(queryStr, SQL_PLAYER);
if (pMsg->Get()->uiNumRows)
{
if (!pMsg->Get()->pSQLResult)
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
MYSQL_ROW row = mysql_fetch_row(pMsg->Get()->pSQLResult);
if (*row[0] != '0')
{
peer->EncodeHeader(DG::PLAYER_CREATE_ALREADY, dwHandle, 0);
return;
}
}
else
unsigned long long count = 0;
if (!CountPlayersByNameExcludingId(p->name, p->pid, &count))
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
snprintf(queryStr, sizeof(queryStr),
"UPDATE player%s SET name='%s',change_name=0 WHERE id=%u", GetTablePostfix(), p->name, p->pid);
if (count != 0)
{
peer->EncodeHeader(DG::PLAYER_CREATE_ALREADY, dwHandle, 0);
return;
}
auto pMsg0 = CDBManager::instance().DirectQuery(queryStr, SQL_PLAYER);
if (!UpdatePlayerName(p->pid, p->name))
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
TPacketDGChangeName pdg;
peer->EncodeHeader(DG::CHANGE_NAME, dwHandle, sizeof(TPacketDGChangeName));
@@ -509,4 +560,3 @@ void CClientManager::QUERY_CHANGE_NAME(CPeer * peer, DWORD dwHandle, TPacketGDCh
strlcpy(pdg.name, p->name, sizeof(pdg.name));
peer->Encode(&pdg, sizeof(TPacketDGChangeName));
}

View File

@@ -8,6 +8,7 @@
#include "ItemAwardManager.h"
#include "HB.h"
#include "Cache.h"
#include "libsql/Statement.h"
extern bool g_bHotBackup;
@@ -15,6 +16,246 @@ extern std::string g_stLocale;
extern int g_test_server;
extern int g_log;
namespace
{
bool PreparePlayerStmt(CStmt& stmt, const char* query)
{
CAsyncSQL* sql = CDBManager::instance().GetDirectSQL(SQL_PLAYER);
if (!sql)
{
sys_err("player SQL handle is not initialized");
return false;
}
return stmt.Prepare(sql, query);
}
bool LoadPlayerIndexSlot(DWORD accountId, BYTE accountIndex, DWORD* playerId, bool* foundRow)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "SELECT pid%u FROM player_index%s WHERE id = ?", accountIndex + 1, GetTablePostfix());
*playerId = 0;
*foundRow = false;
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &accountId))
return false;
if (!stmt.BindResult(MYSQL_TYPE_LONG, playerId))
return false;
if (!stmt.Execute())
return false;
if (stmt.iRows == 0)
return true;
if (!stmt.Fetch())
return false;
*foundRow = true;
return true;
}
bool CountPlayersByName(const char* playerName, unsigned long long* count)
{
char query[QUERY_MAX_LEN];
if (g_stLocale == "sjis")
snprintf(query, sizeof(query),
"SELECT COUNT(*) FROM player%s WHERE name = ? COLLATE sjis_japanese_ci",
GetTablePostfix());
else
snprintf(query, sizeof(query),
"SELECT COUNT(*) FROM player%s WHERE name = ?",
GetTablePostfix());
*count = 0;
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, const_cast<char*>(playerName), CHARACTER_NAME_MAX_LEN + 1))
return false;
if (!stmt.BindResult(MYSQL_TYPE_LONGLONG, count))
return false;
if (!stmt.Execute())
return false;
if (stmt.iRows == 0)
return false;
return stmt.Fetch();
}
bool InsertPlayerRecord(const TPlayerCreatePacket* packet, DWORD* playerId)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query),
"INSERT INTO player%s "
"(id, account_id, name, level, st, ht, dx, iq, "
"job, voice, dir, x, y, z, "
"hp, mp, random_hp, random_sp, stat_point, stamina, part_base, part_main, part_hair, gold, playtime, "
"skill_level, quickslot) "
"VALUES(0, %u, ?, %d, %d, %d, %d, %d, "
"%d, %d, %d, %d, %d, %d, %d, "
"%d, %d, %d, %d, %d, %d, %d, 0, %d, 0, ?, ?)",
GetTablePostfix(),
packet->account_id,
packet->player_table.level,
packet->player_table.st,
packet->player_table.ht,
packet->player_table.dx,
packet->player_table.iq,
packet->player_table.job,
packet->player_table.voice,
packet->player_table.dir,
packet->player_table.x,
packet->player_table.y,
packet->player_table.z,
packet->player_table.hp,
packet->player_table.sp,
packet->player_table.sRandomHP,
packet->player_table.sRandomSP,
packet->player_table.stat_point,
packet->player_table.stamina,
packet->player_table.part_base,
packet->player_table.part_base,
packet->player_table.gold);
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, const_cast<char*>(packet->player_table.name), CHARACTER_NAME_MAX_LEN + 1))
return false;
if (!stmt.BindParam(MYSQL_TYPE_BLOB, const_cast<TPlayerSkill*>(packet->player_table.skills), sizeof(packet->player_table.skills)))
return false;
if (!stmt.BindParam(MYSQL_TYPE_BLOB, const_cast<TQuickslot*>(packet->player_table.quickslot), sizeof(packet->player_table.quickslot)))
return false;
if (!stmt.Execute())
return false;
if (stmt.GetAffectedRows() == 0)
return false;
*playerId = static_cast<DWORD>(stmt.GetInsertId());
return *playerId != 0;
}
bool UpdatePlayerIndexSlot(DWORD accountId, BYTE accountIndex, DWORD playerId)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "UPDATE player_index%s SET pid%u = ? WHERE id = ?", GetTablePostfix(), accountIndex + 1);
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &playerId))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &accountId))
return false;
if (!stmt.Execute())
return false;
return stmt.GetAffectedRows() != 0;
}
bool ArchiveDeletedPlayerById(DWORD playerId)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "INSERT INTO player%s_deleted SELECT * FROM player%s WHERE id = ?",
GetTablePostfix(), GetTablePostfix());
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &playerId))
return false;
if (!stmt.Execute())
return false;
const unsigned long long affectedRows = stmt.GetAffectedRows();
return affectedRows != 0 && affectedRows != static_cast<unsigned long long>(-1);
}
void DeletePlayerById(DWORD playerId)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "DELETE FROM player%s WHERE id = ?", GetTablePostfix());
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &playerId))
return;
stmt.Execute();
}
bool ResetPlayerIndexSlotForDelete(BYTE accountIndex, DWORD playerId)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "UPDATE player_index%s SET pid%u = 0 WHERE pid%u = ?",
GetTablePostfix(), accountIndex + 1, accountIndex + 1);
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &playerId))
return false;
if (!stmt.Execute())
return false;
const unsigned long long affectedRows = stmt.GetAffectedRows();
return affectedRows != 0 && affectedRows != static_cast<unsigned long long>(-1);
}
void DeletePlayerItemsByOwnerId(DWORD playerId)
{
char query[QUERY_MAX_LEN];
snprintf(query, sizeof(query), "DELETE FROM item%s WHERE owner_id = ? AND (window < ? OR window = ?)",
GetTablePostfix());
int32_t safeboxWindow = SAFEBOX;
int32_t dragonSoulInventory = DRAGON_SOUL_INVENTORY;
CStmt stmt;
if (!PreparePlayerStmt(stmt, query))
return;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &playerId))
return;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &safeboxWindow))
return;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &dragonSoulInventory))
return;
stmt.Execute();
}
}
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!!!!!!!!! IMPORTANT !!!!!!!!!!!!
@@ -810,9 +1051,7 @@ static time_by_id_map_t s_createTimeByAccountID;
*/
void CClientManager::__QUERY_PLAYER_CREATE(CPeer *peer, DWORD dwHandle, TPlayerCreatePacket* packet)
{
char queryStr[QUERY_MAX_LEN];
int queryLen;
int player_id;
DWORD player_id = 0;
// 한 계정에 X초 내로 캐릭터 생성을 할 수 없다.
time_by_id_map_t::iterator it = s_createTimeByAccountID.find(packet->account_id);
@@ -828,81 +1067,40 @@ void CClientManager::__QUERY_PLAYER_CREATE(CPeer *peer, DWORD dwHandle, TPlayerC
}
}
queryLen = snprintf(queryStr, sizeof(queryStr),
"SELECT pid%u FROM player_index%s WHERE id=%d", packet->account_index + 1, GetTablePostfix(), packet->account_id);
auto pMsg0 = CDBManager::instance().DirectQuery(queryStr);
if (pMsg0->Get()->uiNumRows != 0)
{
if (!pMsg0->Get()->pSQLResult)
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
MYSQL_ROW row = mysql_fetch_row(pMsg0->Get()->pSQLResult);
DWORD dwPID = 0; str_to_number(dwPID, row[0]);
if (row[0] && dwPID > 0)
{
peer->EncodeHeader(DG::PLAYER_CREATE_ALREADY, dwHandle, 0);
sys_log(0, "ALREADY EXIST AccountChrIdx %d ID %d", packet->account_index, dwPID);
return;
}
}
else
DWORD existingPlayerId = 0;
bool foundPlayerIndexRow = false;
if (!LoadPlayerIndexSlot(packet->account_id, packet->account_index, &existingPlayerId, &foundPlayerIndexRow))
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
if (g_stLocale == "sjis")
snprintf(queryStr, sizeof(queryStr),
"SELECT COUNT(*) as count FROM player%s WHERE name='%s' collate sjis_japanese_ci",
GetTablePostfix(), packet->player_table.name);
else
snprintf(queryStr, sizeof(queryStr),
"SELECT COUNT(*) as count FROM player%s WHERE name='%s'", GetTablePostfix(), packet->player_table.name);
auto pMsg1 = CDBManager::instance().DirectQuery(queryStr);
if (pMsg1->Get()->uiNumRows)
{
if (!pMsg1->Get()->pSQLResult)
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
MYSQL_ROW row = mysql_fetch_row(pMsg1->Get()->pSQLResult);
if (*row[0] != '0')
{
sys_log(0, "ALREADY EXIST name %s, row[0] %s query %s", packet->player_table.name, row[0], queryStr);
peer->EncodeHeader(DG::PLAYER_CREATE_ALREADY, dwHandle, 0);
return;
}
}
else
if (!foundPlayerIndexRow)
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
queryLen = snprintf(queryStr, sizeof(queryStr),
"INSERT INTO player%s "
"(id, account_id, name, level, st, ht, dx, iq, "
"job, voice, dir, x, y, z, "
"hp, mp, random_hp, random_sp, stat_point, stamina, part_base, part_main, part_hair, gold, playtime, "
"skill_level, quickslot) "
"VALUES(0, %u, '%s', %d, %d, %d, %d, %d, "
"%d, %d, %d, %d, %d, %d, %d, "
"%d, %d, %d, %d, %d, %d, %d, 0, %d, 0, ",
GetTablePostfix(),
packet->account_id, packet->player_table.name, packet->player_table.level, packet->player_table.st, packet->player_table.ht, packet->player_table.dx, packet->player_table.iq,
packet->player_table.job, packet->player_table.voice, packet->player_table.dir, packet->player_table.x, packet->player_table.y, packet->player_table.z,
packet->player_table.hp, packet->player_table.sp, packet->player_table.sRandomHP, packet->player_table.sRandomSP, packet->player_table.stat_point, packet->player_table.stamina, packet->player_table.part_base, packet->player_table.part_base, packet->player_table.gold);
if (existingPlayerId > 0)
{
peer->EncodeHeader(DG::PLAYER_CREATE_ALREADY, dwHandle, 0);
sys_log(0, "ALREADY EXIST AccountChrIdx %d ID %d", packet->account_index, existingPlayerId);
return;
}
unsigned long long playerCount = 0;
if (!CountPlayersByName(packet->player_table.name, &playerCount))
{
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
if (playerCount != 0)
{
sys_log(0, "ALREADY EXIST name %s", packet->player_table.name);
peer->EncodeHeader(DG::PLAYER_CREATE_ALREADY, dwHandle, 0);
return;
}
sys_log(0, "PlayerCreate accountid %d name %s level %d gold %d, st %d ht %d job %d",
packet->account_id,
@@ -913,40 +1111,16 @@ void CClientManager::__QUERY_PLAYER_CREATE(CPeer *peer, DWORD dwHandle, TPlayerC
packet->player_table.ht,
packet->player_table.job);
static char text[8192 + 1];
CDBManager::instance().EscapeString(text, packet->player_table.skills, sizeof(packet->player_table.skills));
queryLen += snprintf(queryStr + queryLen, sizeof(queryStr) - queryLen, "'%s', ", text);
if (g_test_server)
sys_log(0, "Create_Player queryLen[%d] TEXT[%s]", queryLen, text);
CDBManager::instance().EscapeString(text, packet->player_table.quickslot, sizeof(packet->player_table.quickslot));
queryLen += snprintf(queryStr + queryLen, sizeof(queryStr) - queryLen, "'%s')", text);
auto pMsg2 = CDBManager::instance().DirectQuery(queryStr);
if (g_test_server)
sys_log(0, "Create_Player queryLen[%d] TEXT[%s]", queryLen, text);
if (pMsg2->Get()->uiAffectedRows <= 0)
if (!InsertPlayerRecord(packet, &player_id))
{
peer->EncodeHeader(DG::PLAYER_CREATE_ALREADY, dwHandle, 0);
sys_log(0, "ALREADY EXIST3 query: %s AffectedRows %lu", queryStr, pMsg2->Get()->uiAffectedRows);
sys_log(0, "ALREADY EXIST3 name %s", packet->player_table.name);
return;
}
player_id = pMsg2->Get()->uiInsertID;
snprintf(queryStr, sizeof(queryStr), "UPDATE player_index%s SET pid%d=%d WHERE id=%d",
GetTablePostfix(), packet->account_index + 1, player_id, packet->account_id);
auto pMsg3 = CDBManager::instance().DirectQuery(queryStr);
if (pMsg3->Get()->uiAffectedRows <= 0)
if (!UpdatePlayerIndexSlot(packet->account_id, packet->account_index, player_id))
{
sys_err("QUERY_ERROR: %s", queryStr);
snprintf(queryStr, sizeof(queryStr), "DELETE FROM player%s WHERE id=%d", GetTablePostfix(), player_id);
CDBManager::instance().DirectQuery(queryStr);
DeletePlayerById(player_id);
peer->EncodeHeader(DG::PLAYER_CREATE_FAILED, dwHandle, 0);
return;
}
@@ -1083,11 +1257,7 @@ void CClientManager::__RESULT_PLAYER_DELETE(CPeer *peer, SQLMsg* msg)
char queryStr[QUERY_MAX_LEN];
snprintf(queryStr, sizeof(queryStr), "INSERT INTO player%s_deleted SELECT * FROM player%s WHERE id=%d",
GetTablePostfix(), GetTablePostfix(), pi->player_id);
auto pIns = CDBManager::instance().DirectQuery(queryStr);
if (pIns->Get()->uiAffectedRows == 0 || pIns->Get()->uiAffectedRows == (uint32_t)-1)
if (!ArchiveDeletedPlayerById(pi->player_id))
{
sys_log(0, "PLAYER_DELETE FAILED %u CANNOT INSERT TO player%s_deleted", dwPID, GetTablePostfix());
@@ -1099,10 +1269,6 @@ void CClientManager::__RESULT_PLAYER_DELETE(CPeer *peer, SQLMsg* msg)
// 삭제 성공
sys_log(0, "PLAYER_DELETE SUCCESS %u", dwPID);
char account_index_string[16];
snprintf(account_index_string, sizeof(account_index_string), "player_id%d", m_iPlayerIDStart + pi->account_index);
// 플레이어 테이블을 캐쉬에서 삭제한다.
CPlayerTableCache * pkPlayerCache = GetPlayerCache(pi->player_id);
@@ -1131,15 +1297,7 @@ void CClientManager::__RESULT_PLAYER_DELETE(CPeer *peer, SQLMsg* msg)
m_map_pkItemCacheSetPtr.erase(pi->player_id);
}
snprintf(queryStr, sizeof(queryStr), "UPDATE player_index%s SET pid%u=0 WHERE pid%u=%d",
GetTablePostfix(),
pi->account_index + 1,
pi->account_index + 1,
pi->player_id);
auto pMsg = CDBManager::instance().DirectQuery(queryStr);
if (pMsg->Get()->uiAffectedRows == 0 || pMsg->Get()->uiAffectedRows == (uint32_t)-1)
if (!ResetPlayerIndexSlotForDelete(pi->account_index, pi->player_id))
{
sys_log(0, "PLAYER_DELETE FAIL WHEN UPDATE account table");
peer->EncodeHeader(DG::PLAYER_DELETE_FAILED, pi->dwHandle, 1);
@@ -1147,11 +1305,9 @@ void CClientManager::__RESULT_PLAYER_DELETE(CPeer *peer, SQLMsg* msg)
return;
}
snprintf(queryStr, sizeof(queryStr), "DELETE FROM player%s WHERE id=%d", GetTablePostfix(), pi->player_id);
CDBManager::instance().DirectQuery(queryStr);
DeletePlayerById(pi->player_id);
snprintf(queryStr, sizeof(queryStr), "DELETE FROM item%s WHERE owner_id=%d AND (window < %d or window = %d)", GetTablePostfix(), pi->player_id, SAFEBOX, DRAGON_SOUL_INVENTORY);
CDBManager::instance().DirectQuery(queryStr);
DeletePlayerItemsByOwnerId(pi->player_id);
snprintf(queryStr, sizeof(queryStr), "DELETE FROM quest%s WHERE dwPID=%d", GetTablePostfix(), pi->player_id);
CDBManager::instance().AsyncQuery(queryStr);
@@ -1372,4 +1528,3 @@ void CClientManager::FlushPlayerCacheSet(DWORD pid)
delete c;
}
}

View File

@@ -4,6 +4,26 @@
extern std::string g_stLocale;
namespace
{
const char* SQLSlotName(int slot)
{
switch (slot)
{
case SQL_PLAYER:
return "player";
case SQL_ACCOUNT:
return "account";
case SQL_COMMON:
return "common";
case SQL_HOTBACKUP:
return "hotbackup";
default:
return "unknown";
}
}
}
CDBManager::CDBManager()
{
Initialize();
@@ -45,6 +65,16 @@ void CDBManager::Quit()
{
for (int i = 0; i < SQL_MAX_NUM; ++i)
{
sys_log(0,
"[SHUTDOWN] DBManager slot=%s begin main_pending=%u main_copied=%u main_results=%u async_pending=%u async_copied=%u async_results=%u",
SQLSlotName(i),
m_mainSQL[i] ? m_mainSQL[i]->CountQuery() : 0,
m_mainSQL[i] ? m_mainSQL[i]->CountCopiedQueryQueue() : 0,
m_mainSQL[i] ? m_mainSQL[i]->CountResult() : 0,
m_asyncSQL[i] ? m_asyncSQL[i]->CountQuery() : 0,
m_asyncSQL[i] ? m_asyncSQL[i]->CountCopiedQueryQueue() : 0,
m_asyncSQL[i] ? m_asyncSQL[i]->CountResult() : 0);
if (m_mainSQL[i])
m_mainSQL[i]->Quit();
@@ -53,6 +83,8 @@ void CDBManager::Quit()
if (m_directSQL[i])
m_directSQL[i]->Quit();
sys_log(0, "[SHUTDOWN] DBManager slot=%s done", SQLSlotName(i));
}
}
@@ -182,4 +214,3 @@ void CDBManager::QueryLocaleSet()
m_asyncSQL[n]->QueryLocaleSet();
}
}

View File

@@ -43,6 +43,7 @@ class CDBManager : public singleton<CDBManager>
void ReturnQuery(const char * c_pszQuery, int iType, IDENT dwIdent, void * pvData, int iSlot = SQL_PLAYER);
void AsyncQuery(const char * c_pszQuery, int iSlot = SQL_PLAYER);
std::unique_ptr<SQLMsg> DirectQuery(const char* c_pszQuery, int iSlot = SQL_PLAYER);
CAsyncSQL* GetDirectSQL(int iSlot = SQL_PLAYER) { return m_directSQL[iSlot].get(); }
SQLMsg * PopResult();
SQLMsg * PopResult(eSQL_SLOT slot );

View File

@@ -3,12 +3,83 @@
#include "DBManager.h"
#include "ItemAwardManager.h"
#include "Peer.h"
#include "libsql/Statement.h"
#include "ClientManager.h"
DWORD g_dwLastCachedItemAwardID = 0;
namespace
{
bool FallbackItemAwardLoadQuery()
{
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery),
"SELECT id,login,vnum,count,socket0,socket1,socket2,mall,why "
"FROM item_award WHERE taken_time IS NULL and id > %d",
g_dwLastCachedItemAwardID);
CDBManager::instance().ReturnQuery(szQuery, QID_ITEM_AWARD_LOAD, 0, NULL);
return true;
}
bool FallbackItemAwardTakenQuery(DWORD dwAwardID, DWORD dwItemID)
{
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery),
"UPDATE item_award SET taken_time=NOW(),item_id=%u WHERE id=%u AND taken_time IS NULL",
dwItemID, dwAwardID);
CDBManager::instance().ReturnQuery(szQuery, QID_ITEM_AWARD_TAKEN, 0, NULL);
return true;
}
void ProcessItemAwardRow(DWORD dwID, const char* login, DWORD dwVnum, DWORD dwCount,
DWORD dwSocket0, DWORD dwSocket1, DWORD dwSocket2, bool bMall, const char* why)
{
if (ItemAwardManager::instance().GetMapAward().find(dwID) != ItemAwardManager::instance().GetMapAward().end())
return;
TItemAward* kData = new TItemAward;
memset(kData, 0, sizeof(TItemAward));
kData->dwID = dwID;
trim_and_lower(login, kData->szLogin, sizeof(kData->szLogin));
kData->dwVnum = dwVnum;
kData->dwCount = dwCount;
kData->dwSocket0 = dwSocket0;
kData->dwSocket1 = dwSocket1;
kData->dwSocket2 = dwSocket2;
kData->bMall = bMall;
if (why && *why)
{
strlcpy(kData->szWhy, why, sizeof(kData->szWhy));
char* whyStr = kData->szWhy;
char cmdStr[100] = "";
strcpy(cmdStr, whyStr);
char command[20] = "";
strcpy(command, CClientManager::instance().GetCommand(cmdStr));
if (!(strcmp(command, "GIFT")))
{
TPacketItemAwardInfromer giftData;
strcpy(giftData.login, kData->szLogin);
strcpy(giftData.command, command);
giftData.vnum = kData->dwVnum;
CClientManager::instance().ForwardPacket(DG::ITEMAWARD_INFORMER, &giftData, sizeof(TPacketItemAwardInfromer));
}
}
ItemAwardManager::instance().GetMapAward().insert(std::make_pair(dwID, kData));
printf("ITEM_AWARD load id %u bMall %d \n", kData->dwID, kData->bMall);
sys_log(0, "ITEM_AWARD: load id %lu login %s vnum %lu count %u socket %lu", kData->dwID, kData->szLogin, kData->dwVnum, kData->dwCount, kData->dwSocket0);
std::set<TItemAward*>& kSet = ItemAwardManager::instance().GetMapkSetAwardByLogin()[kData->szLogin];
kSet.insert(kData);
if (dwID > g_dwLastCachedItemAwardID)
g_dwLastCachedItemAwardID = dwID;
}
}
ItemAwardManager::ItemAwardManager()
{
}
@@ -19,9 +90,45 @@ ItemAwardManager::~ItemAwardManager()
void ItemAwardManager::RequestLoad()
{
char szQuery[QUERY_MAX_LEN];
snprintf(szQuery, sizeof(szQuery), "SELECT id,login,vnum,count,socket0,socket1,socket2,mall,why FROM item_award WHERE taken_time IS NULL and id > %d", g_dwLastCachedItemAwardID);
CDBManager::instance().ReturnQuery(szQuery, QID_ITEM_AWARD_LOAD, 0, NULL);
static const char* query =
"SELECT id, login, vnum, count, socket0, socket1, socket2, mall, COALESCE(why, '') "
"FROM item_award WHERE taken_time IS NULL AND id > ?";
CStmt stmt;
DWORD dwID = 0;
DWORD dwVnum = 0;
DWORD dwCount = 0;
DWORD dwSocket0 = 0;
DWORD dwSocket1 = 0;
DWORD dwSocket2 = 0;
char login[LOGIN_MAX_LEN + 1] = {};
char why[ITEM_AWARD_WHY_MAX_LEN + 1] = {};
DWORD dwMall = 0;
if (!stmt.Prepare(CDBManager::instance().GetDirectSQL(), query) ||
!stmt.BindParam(MYSQL_TYPE_LONG, &g_dwLastCachedItemAwardID) ||
!stmt.BindResult(MYSQL_TYPE_LONG, &dwID) ||
!stmt.BindResult(MYSQL_TYPE_STRING, login, sizeof(login)) ||
!stmt.BindResult(MYSQL_TYPE_LONG, &dwVnum) ||
!stmt.BindResult(MYSQL_TYPE_LONG, &dwCount) ||
!stmt.BindResult(MYSQL_TYPE_LONG, &dwSocket0) ||
!stmt.BindResult(MYSQL_TYPE_LONG, &dwSocket1) ||
!stmt.BindResult(MYSQL_TYPE_LONG, &dwSocket2) ||
!stmt.BindResult(MYSQL_TYPE_LONG, &dwMall) ||
!stmt.BindResult(MYSQL_TYPE_STRING, why, sizeof(why)) ||
!stmt.Execute())
{
sys_err("ITEM_AWARD: prepared load failed, falling back to legacy query path");
FallbackItemAwardLoadQuery();
return;
}
while (stmt.Fetch())
{
ProcessItemAwardRow(dwID, login, dwVnum, dwCount, dwSocket0, dwSocket1, dwSocket2, dwMall != 0, why);
memset(login, 0, sizeof(login));
memset(why, 0, sizeof(why));
}
}
void ItemAwardManager::Load(SQLMsg * pMsg)
@@ -35,51 +142,22 @@ void ItemAwardManager::Load(SQLMsg * pMsg)
DWORD dwID = 0;
str_to_number(dwID, row[col++]);
const char* login = row[col++];
if (m_map_award.find(dwID) != m_map_award.end())
continue;
DWORD dwVnum = 0;
DWORD dwCount = 0;
DWORD dwSocket0 = 0;
DWORD dwSocket1 = 0;
DWORD dwSocket2 = 0;
DWORD dwMall = 0;
str_to_number(dwVnum, row[col++]);
str_to_number(dwCount, row[col++]);
str_to_number(dwSocket0, row[col++]);
str_to_number(dwSocket1, row[col++]);
str_to_number(dwSocket2, row[col++]);
str_to_number(dwMall, row[col++]);
TItemAward * kData = new TItemAward;
memset(kData, 0, sizeof(TItemAward));
kData->dwID = dwID;
trim_and_lower(row[col++], kData->szLogin, sizeof(kData->szLogin));
str_to_number(kData->dwVnum, row[col++]);
str_to_number(kData->dwCount, row[col++]);
str_to_number(kData->dwSocket0, row[col++]);
str_to_number(kData->dwSocket1, row[col++]);
str_to_number(kData->dwSocket2, row[col++]);
str_to_number(kData->bMall, row[col++]);
if (row[col])
{
strlcpy(kData->szWhy, row[col], sizeof(kData->szWhy));
//게임 중에 why콜룸에 변동이 생기면
char* whyStr = kData->szWhy; //why 콜룸 읽기
char cmdStr[100] = ""; //why콜룸에서 읽은 값을 임시 문자열에 복사해둠
strcpy(cmdStr,whyStr); //명령어 얻는 과정에서 토큰쓰면 원본도 토큰화 되기 때문
char command[20] = "";
strcpy(command,CClientManager::instance().GetCommand(cmdStr)); // command 얻기
//sys_err("%d, %s",pItemAward->dwID,command);
if( !(strcmp(command,"GIFT") )) // command 가 GIFT이면
{
TPacketItemAwardInfromer giftData;
strcpy(giftData.login, kData->szLogin); //로그인 아이디 복사
strcpy(giftData.command, command); //명령어 복사
giftData.vnum = kData->dwVnum; //아이템 vnum도 복사
CClientManager::instance().ForwardPacket(DG::ITEMAWARD_INFORMER,&giftData,sizeof(TPacketItemAwardInfromer));
}
}
m_map_award.insert(std::make_pair(dwID, kData));
printf("ITEM_AWARD load id %u bMall %d \n", kData->dwID, kData->bMall);
sys_log(0, "ITEM_AWARD: load id %lu login %s vnum %lu count %u socket %lu", kData->dwID, kData->szLogin, kData->dwVnum, kData->dwCount, kData->dwSocket0);
std::set<TItemAward *> & kSet = m_map_kSetAwardByLogin[kData->szLogin];
kSet.insert(kData);
if (dwID > g_dwLastCachedItemAwardID)
g_dwLastCachedItemAwardID = dwID;
ProcessItemAwardRow(dwID, login, dwVnum, dwCount, dwSocket0, dwSocket1, dwSocket2, dwMall != 0, row[col] ? row[col] : "");
}
}
@@ -109,13 +187,18 @@ void ItemAwardManager::Taken(DWORD dwAwardID, DWORD dwItemID)
//
// Update taken_time in database to prevent not to give him again.
//
char szQuery[QUERY_MAX_LEN];
static const char* query =
"UPDATE item_award SET taken_time=NOW(), item_id=? WHERE id=? AND taken_time IS NULL";
snprintf(szQuery, sizeof(szQuery),
"UPDATE item_award SET taken_time=NOW(),item_id=%u WHERE id=%u AND taken_time IS NULL",
dwItemID, dwAwardID);
CDBManager::instance().ReturnQuery(szQuery, QID_ITEM_AWARD_TAKEN, 0, NULL);
CStmt stmt;
if (!stmt.Prepare(CDBManager::instance().GetDirectSQL(), query) ||
!stmt.BindParam(MYSQL_TYPE_LONG, &dwItemID) ||
!stmt.BindParam(MYSQL_TYPE_LONG, &dwAwardID) ||
!stmt.Execute())
{
sys_err("ITEM_AWARD: prepared taken update failed, falling back to legacy query path");
FallbackItemAwardTakenQuery(dwAwardID, dwItemID);
}
}
std::map<DWORD, TItemAward *>& ItemAwardManager::GetMapAward()
@@ -126,4 +209,4 @@ std::map<DWORD, TItemAward *>& ItemAwardManager::GetMapAward()
std::map<std::string, std::set<TItemAward *> >& ItemAwardManager::GetMapkSetAwardByLogin()
{
return m_map_kSetAwardByLogin;
}
}

View File

@@ -45,6 +45,158 @@ extern const char * _malloc_options;
extern void WriteVersion();
namespace
{
struct SQLConnectionConfig
{
char host[64];
char database[64];
char user[64];
char password[64];
int port;
};
const char* BoolState(bool value)
{
return value ? "on" : "off";
}
const char* EmptyToLabel(const std::string& value, const char* fallback)
{
return value.empty() ? fallback : value.c_str();
}
bool CopyEnvString(const char* env_name, char* dest, size_t dest_size)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
strlcpy(dest, value, dest_size);
return true;
}
bool CopyEnvInt(const char* env_name, int* dest)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
str_to_number(*dest, value);
return true;
}
bool HasSQLConfig(const SQLConnectionConfig& config)
{
return config.host[0] && config.database[0] && config.user[0] && config.password[0];
}
bool ParseSQLConfig(const char* value, SQLConnectionConfig* config, const char* label)
{
int token_count = sscanf(
value,
" %63s %63s %63s %63s %d ",
config->host,
config->database,
config->user,
config->password,
&config->port);
if (token_count < 4 || !HasSQLConfig(*config))
{
fprintf(stderr, "%s syntax: <host db user password [port]>\n", label);
return false;
}
return true;
}
bool LoadSQLConfig(const char* key, const char* env_prefix, SQLConnectionConfig* config)
{
char line[256 + 1];
bool loaded_from_file = false;
if (CConfig::instance().GetValue(key, line, sizeof(line) - 1))
{
if (!ParseSQLConfig(line, config, key))
return false;
loaded_from_file = true;
}
bool overridden = false;
const std::string prefix = env_prefix;
overridden |= CopyEnvString((prefix + "_HOST").c_str(), config->host, sizeof(config->host));
overridden |= CopyEnvString((prefix + "_DB").c_str(), config->database, sizeof(config->database));
overridden |= CopyEnvString((prefix + "_USER").c_str(), config->user, sizeof(config->user));
overridden |= CopyEnvString((prefix + "_PASSWORD").c_str(), config->password, sizeof(config->password));
overridden |= CopyEnvInt((prefix + "_PORT").c_str(), &config->port);
if (overridden)
sys_log(0, "CONFIG: %s overridden from environment", key);
if (!loaded_from_file && !overridden)
{
sys_err("%s not configured", key);
return false;
}
if (!HasSQLConfig(*config))
{
sys_err("%s incomplete after applying config/environment overrides", key);
return false;
}
return true;
}
bool ConnectDatabase(int slot, const SQLConnectionConfig& config, const char* label)
{
sys_log(0, "connecting to MySQL server (%s)", label);
int retry_count = 5;
do
{
if (CDBManager::instance().Connect(slot, config.host, config.port, config.database, config.user, config.password))
{
sys_log(0, " OK");
return true;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
}
while (retry_count--);
return false;
}
void LogStartupSummary(int heart_fps, int player_id_start)
{
sys_log(0,
"[STARTUP] locale=%s table_postfix=%s player_db=%s player_id_start=%d heart_fps=%d test_server=%s log=%s hotbackup=%s",
EmptyToLabel(g_stLocale, "<unset>"),
EmptyToLabel(g_stTablePostfix, "<none>"),
EmptyToLabel(g_stPlayerDBName, "<unset>"),
player_id_start,
heart_fps,
BoolState(g_test_server),
BoolState(g_log != 0),
BoolState(g_bHotBackup)
);
sys_log(0,
"[STARTUP] cache_flush player=%d item=%d pricelist=%d logout=%d locale_name_column=%s",
g_iPlayerCacheFlushSeconds,
g_iItemCacheFlushSeconds,
g_iItemPriceListTableCacheFlushSeconds,
g_iLogoutSeconds,
EmptyToLabel(g_stLocaleNameColumn, "<unset>")
);
}
}
void emergency_sig(int sig)
{
if (sig == SIGSEGV)
@@ -91,7 +243,9 @@ int main()
signal_timer_disable();
sys_log(0, "[SHUTDOWN] DB main loop finished, quitting SQL workers");
DBManager.Quit();
sys_log(0, "[SHUTDOWN] DB SQL workers stopped");
int iCount;
while (1)
@@ -108,8 +262,10 @@ int main()
sys_log(0, "WAITING_QUERY_COUNT %d", iCount);
}
sys_log(0, "[SHUTDOWN] DB process exiting cleanly");
log_destroy();
return 1;
return 0;
}
void emptybeat(LPHEART heart, int pulse)
@@ -190,7 +346,7 @@ int Start()
if (!CConfig::instance().GetValue("TABLE_POSTFIX", szBuf, 256))
{
sys_err("TABLE_POSTFIX not configured use default");
sys_log(0, "CONFIG: TABLE_POSTFIX not configured, using default table names");
szBuf[0] = '\0';
}
@@ -237,120 +393,43 @@ int Start()
g_stLocaleNameColumn = szBuf;
}
char szAddr[64], szDB[64], szUser[64], szPassword[64];
int iPort;
char line[256+1];
SQLConnectionConfig player_sql = {};
SQLConnectionConfig account_sql = {};
SQLConnectionConfig common_sql = {};
SQLConnectionConfig hotbackup_sql = {};
if (CConfig::instance().GetValue("SQL_PLAYER", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (player)");
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_PLAYER, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
} while (iRetry--);
fprintf(stderr, "Success PLAYER\n");
SetPlayerDBName(szDB);
}
else
{
sys_err("SQL_PLAYER not configured");
if (!LoadSQLConfig("SQL_PLAYER", "METIN2_PLAYER_SQL", &player_sql))
return false;
}
if (CConfig::instance().GetValue("SQL_ACCOUNT", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (account)");
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_ACCOUNT, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
} while (iRetry--);
fprintf(stderr, "Success ACCOUNT\n");
}
else
{
sys_err("SQL_ACCOUNT not configured");
if (!ConnectDatabase(SQL_PLAYER, player_sql, "player"))
return false;
}
if (CConfig::instance().GetValue("SQL_COMMON", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (common)");
fprintf(stderr, "Success PLAYER\n");
SetPlayerDBName(player_sql.database);
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_COMMON, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
} while (iRetry--);
fprintf(stderr, "Success COMMON\n");
}
else
{
sys_err("SQL_COMMON not configured");
if (!LoadSQLConfig("SQL_ACCOUNT", "METIN2_ACCOUNT_SQL", &account_sql))
return false;
}
if (CConfig::instance().GetValue("SQL_HOTBACKUP", line, 256))
{
sscanf(line, " %s %s %s %s %d ", szAddr, szDB, szUser, szPassword, &iPort);
sys_log(0, "connecting to MySQL server (hotbackup)");
int iRetry = 5;
do
{
if (CDBManager::instance().Connect(SQL_HOTBACKUP, szAddr, iPort, szDB, szUser, szPassword))
{
sys_log(0, " OK");
break;
}
sys_log(0, " failed, retrying in 5 seconds");
fprintf(stderr, " failed, retrying in 5 seconds");
sleep(5);
}
while (iRetry--);
fprintf(stderr, "Success HOTBACKUP\n");
}
else
{
sys_err("SQL_HOTBACKUP not configured");
if (!ConnectDatabase(SQL_ACCOUNT, account_sql, "account"))
return false;
}
fprintf(stderr, "Success ACCOUNT\n");
if (!LoadSQLConfig("SQL_COMMON", "METIN2_COMMON_SQL", &common_sql))
return false;
if (!ConnectDatabase(SQL_COMMON, common_sql, "common"))
return false;
fprintf(stderr, "Success COMMON\n");
if (!LoadSQLConfig("SQL_HOTBACKUP", "METIN2_HOTBACKUP_SQL", &hotbackup_sql))
return false;
if (!ConnectDatabase(SQL_HOTBACKUP, hotbackup_sql, "hotbackup"))
return false;
fprintf(stderr, "Success HOTBACKUP\n");
if (!CNetPoller::instance().Create())
{
@@ -374,6 +453,8 @@ int Start()
return false;
}
LogStartupSummary(heart_beat, iIDStart);
#ifndef OS_WINDOWS
signal(SIGUSR1, emergency_sig);
#endif
@@ -409,4 +490,3 @@ const char * GetPlayerDBName()
{
return g_stPlayerDBName.c_str();
}

View File

@@ -37,6 +37,10 @@ bool CNetPoller::Create()
return false;
}
sys_log(0, "[STARTUP] fdwatch backend=%s descriptor_limit=%d",
fdwatch_backend_name(fdwatch_get_backend(m_fdWatcher)),
fdwatch_get_descriptor_limit(m_fdWatcher));
return true;
}

View File

@@ -105,7 +105,10 @@ int CPeerBase::Recv()
if (bytes_read < 0)
{
sys_err("socket_read failed %s", strerror(errno));
if (errno == 0)
return -2;
sys_err("socket_read failed: host=%s fd=%d errno=%d (%s)", m_host, m_fd, errno, strerror(errno));
return -1;
}
else if (bytes_read == 0)

View File

@@ -40,6 +40,7 @@
#include "xmas_event.h"
#include "banword.h"
#include "target.h"
#include "request_cooldown.h"
#include "wedding.h"
#include "mob_manager.h"
#include "mining.h"
@@ -5698,8 +5699,9 @@ void CHARACTER::ReqSafeboxLoad(const char* pszPassword)
}
int iPulse = thecore_pulse();
const int last_safebox_load_time = GetSafeboxLoadTime();
if (iPulse - GetSafeboxLoadTime() < PASSES_PER_SEC(10))
if (HasRecentRequestCooldown(last_safebox_load_time, iPulse, PASSES_PER_SEC(10)))
{
ChatPacket(CHAT_TYPE_INFO, LC_TEXT("<창고> 창고를 닫은지 10초 안에는 열 수 없습니다."));
return;

View File

@@ -27,6 +27,7 @@
#include "unique_item.h"
#include "threeway_war.h"
#include "log.h"
#include "request_cooldown.h"
#include "common/VnumHelper.h"
extern int g_server_id;
@@ -938,6 +939,7 @@ ACMD(do_mall_password)
}
int iPulse = thecore_pulse();
const int last_mall_load_time = ch->GetMallLoadTime();
if (ch->GetMall())
{
@@ -945,7 +947,7 @@ ACMD(do_mall_password)
return;
}
if (iPulse - ch->GetMallLoadTime() < passes_per_sec * 10) // 10초에 한번만 요청 가능
if (HasRecentRequestCooldown(last_mall_load_time, iPulse, passes_per_sec * 10)) // 10초에 한번만 요청 가능
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("<창고> 창고를 닫은지 10초 안에는 열 수 없습니다."));
return;

View File

@@ -75,7 +75,7 @@ string g_stDefaultQuestObjectDir = "./quest/object";
std::set<string> g_setQuestObjectDir;
std::vector<std::string> g_stAdminPageIP;
std::string g_stAdminPagePassword = "SHOWMETHEMONEY";
std::string g_stAdminPagePassword;
string g_stBlockDate = "30000705";
@@ -195,7 +195,124 @@ static void FN_log_adminpage()
++iter;
}
sys_log(1, "ADMIN_PAGE_PASSWORD = %s", g_stAdminPagePassword.c_str());
sys_log(1, "ADMIN_PAGE_PASSWORD = %s", g_stAdminPagePassword.empty() ? "[disabled]" : "[configured]");
}
static void FN_apply_adminpage_password_env()
{
const char* env_password = std::getenv("METIN2_ADMINPAGE_PASSWORD");
if (!env_password)
return;
g_stAdminPagePassword = env_password;
}
static bool FN_copy_env_string(const char* env_name, char* dest, size_t dest_size)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
strlcpy(dest, value, dest_size);
return true;
}
static bool FN_copy_env_int(const char* env_name, int* dest)
{
const char* value = std::getenv(env_name);
if (!value)
return false;
str_to_number(*dest, value);
return true;
}
static bool FN_has_sql_config(const char* host, const char* user, const char* password, const char* database)
{
return host[0] && user[0] && password[0] && database[0];
}
static bool FN_parse_game_sql_config(
const char* value,
char* host,
size_t host_size,
char* user,
size_t user_size,
char* password,
size_t password_size,
char* database,
size_t database_size,
int* port,
const char* label)
{
const char* line = two_arguments(value, host, host_size, user, user_size);
line = two_arguments(line, password, password_size, database, database_size);
if (line[0])
{
char port_buf[256];
one_argument(line, port_buf, sizeof(port_buf));
str_to_number(*port, port_buf);
}
if (!FN_has_sql_config(host, user, password, database))
{
fprintf(stderr, "%s syntax: <host user password db [port]>\n", label);
return false;
}
return true;
}
static bool FN_apply_game_sql_env(
const char* env_prefix,
char* host,
size_t host_size,
char* user,
size_t user_size,
char* password,
size_t password_size,
char* database,
size_t database_size,
int* port,
const char* label)
{
bool overridden = false;
std::string prefix = env_prefix;
overridden |= FN_copy_env_string((prefix + "_HOST").c_str(), host, host_size);
overridden |= FN_copy_env_string((prefix + "_USER").c_str(), user, user_size);
overridden |= FN_copy_env_string((prefix + "_PASSWORD").c_str(), password, password_size);
overridden |= FN_copy_env_string((prefix + "_DB").c_str(), database, database_size);
overridden |= FN_copy_env_int((prefix + "_PORT").c_str(), port);
if (overridden)
sys_log(0, "CONFIG: %s overridden from environment", label);
return overridden;
}
static void FN_apply_db_runtime_env()
{
const char* env_addr = std::getenv("METIN2_DB_ADDR");
if (env_addr)
{
strlcpy(db_addr, env_addr, sizeof(db_addr));
for (int n = 0; n < ADDRESS_MAX_LEN; ++n)
{
if (db_addr[n] == ' ')
db_addr[n] = '\0';
}
sys_log(0, "CONFIG: DB_ADDR overridden from environment");
}
int env_port = 0;
if (FN_copy_env_int("METIN2_DB_PORT", &env_port))
{
db_port = static_cast<WORD>(env_port);
sys_log(0, "CONFIG: DB_PORT overridden from environment");
}
}
@@ -366,8 +483,10 @@ void config_init(const string& st_localeServiceName)
// DB정보만 읽어와 로케일 세팅을 한후 다른 세팅을 적용시켜야한다.
// 이유는 로케일관련된 초기화 루틴이 곳곳에 존재하기 때문.
bool isCommonSQL = false;
bool isAccountSQL = false;
bool isCommonSQL = false;
bool isPlayerSQL = false;
bool isLogSQL = false;
FILE* fp_common;
if (!(fp_common = fopen("conf/game.txt", "r")))
@@ -381,96 +500,69 @@ void config_init(const string& st_localeServiceName)
TOKEN("account_sql")
{
const char* line = two_arguments(value_string, db_host[0], sizeof(db_host[0]), db_user[0], sizeof(db_user[0]));
line = two_arguments(line, db_pwd[0], sizeof(db_pwd[0]), db_db[0], sizeof(db_db[0]));
if (line[0])
if (!FN_parse_game_sql_config(value_string,
db_host[0], sizeof(db_host[0]),
db_user[0], sizeof(db_user[0]),
db_pwd[0], sizeof(db_pwd[0]),
db_db[0], sizeof(db_db[0]),
&mysql_db_port[0],
"ACCOUNT_SQL"))
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(mysql_db_port[0], buf);
}
if (!*db_host[0] || !*db_user[0] || !*db_pwd[0] || !*db_db[0])
{
fprintf(stderr, "PLAYER_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "PLAYER_SQL: %s %s %s %s %d", db_host[0], db_user[0], db_pwd[0], db_db[0], mysql_db_port[0]);
isPlayerSQL = true;
isAccountSQL = true;
continue;
}
TOKEN("player_sql")
{
const char* line = two_arguments(value_string, db_host[1], sizeof(db_host[1]), db_user[1], sizeof(db_user[1]));
line = two_arguments(line, db_pwd[1], sizeof(db_pwd[1]), db_db[1], sizeof(db_db[1]));
if (line[0])
if (!FN_parse_game_sql_config(value_string,
db_host[1], sizeof(db_host[1]),
db_user[1], sizeof(db_user[1]),
db_pwd[1], sizeof(db_pwd[1]),
db_db[1], sizeof(db_db[1]),
&mysql_db_port[1],
"PLAYER_SQL"))
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(mysql_db_port[1], buf);
}
if (!*db_host[1] || !*db_user[1] || !*db_pwd[1] || !*db_db[1])
{
fprintf(stderr, "PLAYER_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "PLAYER_SQL: %s %s %s %s %d", db_host[1], db_user[1], db_pwd[1], db_db[1], mysql_db_port[1]);
isPlayerSQL = true;
continue;
}
TOKEN("common_sql")
{
const char* line = two_arguments(value_string, db_host[2], sizeof(db_host[2]), db_user[2], sizeof(db_user[2]));
line = two_arguments(line, db_pwd[2], sizeof(db_pwd[2]), db_db[2], sizeof(db_db[2]));
if (line[0])
if (!FN_parse_game_sql_config(value_string,
db_host[2], sizeof(db_host[2]),
db_user[2], sizeof(db_user[2]),
db_pwd[2], sizeof(db_pwd[2]),
db_db[2], sizeof(db_db[2]),
&mysql_db_port[2],
"COMMON_SQL"))
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(mysql_db_port[2], buf);
}
if (!*db_host[2] || !*db_user[2] || !*db_pwd[2] || !*db_db[2])
{
fprintf(stderr, "COMMON_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "COMMON_SQL: %s %s %s %s %d", db_host[2], db_user[2], db_pwd[2], db_db[2], mysql_db_port[2]);
isCommonSQL = true;
continue;
}
TOKEN("log_sql")
{
const char* line = two_arguments(value_string, log_host, sizeof(log_host), log_user, sizeof(log_user));
line = two_arguments(line, log_pwd, sizeof(log_pwd), log_db, sizeof(log_db));
if (line[0])
if (!FN_parse_game_sql_config(value_string,
log_host, sizeof(log_host),
log_user, sizeof(log_user),
log_pwd, sizeof(log_pwd),
log_db, sizeof(log_db),
&log_port,
"LOG_SQL"))
{
char buf[256];
one_argument(line, buf, sizeof(buf));
str_to_number(log_port, buf);
}
if (!*log_host || !*log_user || !*log_pwd || !*log_db)
{
fprintf(stderr, "LOG_SQL syntax: logsql <host user password db>\n");
exit(1);
}
char buf[1024];
snprintf(buf, sizeof(buf), "LOG_SQL: %s %s %s %s %d", log_host, log_user, log_pwd, log_db, log_port);
isLogSQL = true;
continue;
}
@@ -753,6 +845,44 @@ void config_init(const string& st_localeServiceName)
}
fclose(fp_common);
const bool account_env_override = FN_apply_game_sql_env(
"METIN2_ACCOUNT_SQL",
db_host[0], sizeof(db_host[0]),
db_user[0], sizeof(db_user[0]),
db_pwd[0], sizeof(db_pwd[0]),
db_db[0], sizeof(db_db[0]),
&mysql_db_port[0],
"ACCOUNT_SQL");
const bool player_env_override = FN_apply_game_sql_env(
"METIN2_PLAYER_SQL",
db_host[1], sizeof(db_host[1]),
db_user[1], sizeof(db_user[1]),
db_pwd[1], sizeof(db_pwd[1]),
db_db[1], sizeof(db_db[1]),
&mysql_db_port[1],
"PLAYER_SQL");
const bool common_env_override = FN_apply_game_sql_env(
"METIN2_COMMON_SQL",
db_host[2], sizeof(db_host[2]),
db_user[2], sizeof(db_user[2]),
db_pwd[2], sizeof(db_pwd[2]),
db_db[2], sizeof(db_db[2]),
&mysql_db_port[2],
"COMMON_SQL");
const bool log_env_override = FN_apply_game_sql_env(
"METIN2_LOG_SQL",
log_host, sizeof(log_host),
log_user, sizeof(log_user),
log_pwd, sizeof(log_pwd),
log_db, sizeof(log_db),
&log_port,
"LOG_SQL");
isAccountSQL = isAccountSQL || account_env_override || FN_has_sql_config(db_host[0], db_user[0], db_pwd[0], db_db[0]);
isPlayerSQL = isPlayerSQL || player_env_override || FN_has_sql_config(db_host[1], db_user[1], db_pwd[1], db_db[1]);
isCommonSQL = isCommonSQL || common_env_override || FN_has_sql_config(db_host[2], db_user[2], db_pwd[2], db_db[2]);
isLogSQL = isLogSQL || log_env_override || FN_has_sql_config(log_host, log_user, log_pwd, log_db);
FILE* fpOnlyForDB;
if (!(fpOnlyForDB = fopen(st_configFileName.c_str(), "r")))
@@ -826,17 +956,41 @@ void config_init(const string& st_localeServiceName)
exit(1);
}
if (!isPlayerSQL)
if (g_bAuthServer && !isAccountSQL)
{
puts("LOAD_ACCOUNT_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("ACCOUNT_SQL: HOST USER PASSWORD DATABASE [PORT]");
puts("");
exit(1);
}
if (!g_bAuthServer && !isPlayerSQL)
{
puts("LOAD_PLAYER_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("PLAYER_SQL: HOST USER PASSWORD DATABASE");
puts("PLAYER_SQL: HOST USER PASSWORD DATABASE [PORT]");
puts("");
exit(1);
}
if (!g_bAuthServer && !isLogSQL)
{
puts("LOAD_LOG_SQL_INFO_FAILURE:");
puts("");
puts("CONFIG:");
puts("------------------------------------------------");
puts("LOG_SQL: HOST USER PASSWORD DATABASE [PORT]");
puts("");
exit(1);
}
FN_apply_db_runtime_env();
// Common DB 가 Locale 정보를 가지고 있기 때문에 가장 먼저 접속해야 한다.
AccountDB::instance().Connect(db_host[2], mysql_db_port[2], db_user[2], db_pwd[2], db_db[2]);
@@ -1128,6 +1282,7 @@ void config_init(const string& st_localeServiceName)
LoadStateUserCount();
CWarMapManager::instance().LoadWarMapInfo(NULL);
FN_apply_adminpage_password_env();
FN_log_adminpage();
}
@@ -1237,5 +1392,3 @@ bool IsValidFileCRC(DWORD dwCRC)
{
return s_set_dwFileCRC.find(dwCRC) != s_set_dwFileCRC.end();
}

View File

@@ -1,4 +1,5 @@
#include "stdafx.h"
#include <array>
#include <sstream>
#include "common/length.h"
@@ -17,9 +18,211 @@
#include "login_data.h"
#include "locale_service.h"
#include "spam.h"
#include "libsql/Statement.h"
extern std::string g_stBlockDate;
namespace
{
struct AuthLoginData
{
char encryptedPassword[45 + 1] = {};
char password[45 + 1] = {};
char socialId[SOCIAL_ID_MAX_LEN + 1] = {};
char status[ACCOUNT_STATUS_MAX_LEN + 1] = {};
uint32_t accountId = 0;
uint8_t notAvailable = 0;
std::array<int, PREMIUM_MAX_NUM> premiumTimes = {};
long long createTime = 0;
};
bool IsChannelServiceLogin(const char* login)
{
return login && login[0] == '[';
}
bool PrepareGameStmt(CStmt& stmt, const std::string& query)
{
CAsyncSQL* sql = DBManager::instance().GetDirectSQL();
if (!sql)
{
sys_err("game direct SQL handle is not initialized");
return false;
}
return stmt.Prepare(sql, query.c_str());
}
bool LoadAuthLoginData(const char* login, const char* passwd, AuthLoginData& auth, bool& found)
{
CStmt stmt;
const bool channelServiceLogin = IsChannelServiceLogin(login);
const std::string query = channelServiceLogin
? "SELECT ?, password, social_id, id, status, availDt - NOW() > 0,"
"UNIX_TIMESTAMP(silver_expire),"
"UNIX_TIMESTAMP(gold_expire),"
"UNIX_TIMESTAMP(safebox_expire),"
"UNIX_TIMESTAMP(autoloot_expire),"
"UNIX_TIMESTAMP(fish_mind_expire),"
"UNIX_TIMESTAMP(marriage_fast_expire),"
"UNIX_TIMESTAMP(money_drop_rate_expire),"
"UNIX_TIMESTAMP(create_time)"
" FROM account WHERE login=?"
: "SELECT PASSWORD(?), password, social_id, id, status, availDt - NOW() > 0,"
"UNIX_TIMESTAMP(silver_expire),"
"UNIX_TIMESTAMP(gold_expire),"
"UNIX_TIMESTAMP(safebox_expire),"
"UNIX_TIMESTAMP(autoloot_expire),"
"UNIX_TIMESTAMP(fish_mind_expire),"
"UNIX_TIMESTAMP(marriage_fast_expire),"
"UNIX_TIMESTAMP(money_drop_rate_expire),"
"UNIX_TIMESTAMP(create_time)"
" FROM account WHERE login=?";
found = false;
if (!PrepareGameStmt(stmt, query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, const_cast<char*>(passwd))
|| !stmt.BindParam(MYSQL_TYPE_STRING, const_cast<char*>(login)))
{
return false;
}
if (!stmt.BindResult(MYSQL_TYPE_STRING, auth.encryptedPassword, sizeof(auth.encryptedPassword))
|| !stmt.BindResult(MYSQL_TYPE_STRING, auth.password, sizeof(auth.password))
|| !stmt.BindResult(MYSQL_TYPE_STRING, auth.socialId, sizeof(auth.socialId))
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.accountId)
|| !stmt.BindResult(MYSQL_TYPE_STRING, auth.status, sizeof(auth.status))
|| !stmt.BindResult(MYSQL_TYPE_TINY, &auth.notAvailable)
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.premiumTimes[PREMIUM_EXP])
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.premiumTimes[PREMIUM_ITEM])
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.premiumTimes[PREMIUM_SAFEBOX])
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.premiumTimes[PREMIUM_AUTOLOOT])
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.premiumTimes[PREMIUM_FISH_MIND])
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.premiumTimes[PREMIUM_MARRIAGE_FAST])
|| !stmt.BindResult(MYSQL_TYPE_LONG, &auth.premiumTimes[PREMIUM_GOLD])
|| !stmt.BindResult(MYSQL_TYPE_LONGLONG, &auth.createTime))
{
return false;
}
if (!stmt.Execute())
return false;
if (stmt.iRows == 0)
return true;
if (!stmt.Fetch())
return false;
found = true;
return true;
}
void FormatCreateDate(const AuthLoginData& auth, char* dst, size_t dstSize)
{
strlcpy(dst, "00000000", dstSize);
if (auth.createTime <= 0)
return;
time_t createTime = static_cast<time_t>(auth.createTime);
struct tm tmBuf;
if (!localtime_r(&createTime, &tmBuf))
return;
strftime(dst, dstSize, "%Y%m%d", &tmBuf);
}
void UpdateAccountLastPlay(uint32_t accountId)
{
CStmt stmt;
const std::string query = "UPDATE account SET last_play=NOW() WHERE id=?";
if (!PrepareGameStmt(stmt, query))
return;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &accountId))
return;
if (!stmt.Execute())
{
sys_err("failed to update last_play for account %u", accountId);
}
}
void FinalizeAuthLogin(LPDESC d, const char* login, const char* passwd, const AuthLoginData& auth, const char* logPrefix)
{
char createDate[256] = "00000000";
if (LC_IsEurope() || test_server)
{
FormatCreateDate(auth, createDate, sizeof(createDate));
sys_log(0, "Create_Time %lld %s", auth.createTime, createDate);
sys_log(0, "Block Time %d ", strncmp(createDate, g_stBlockDate.c_str(), 8));
}
if (strcmp(auth.encryptedPassword, auth.password))
{
RecordLoginFailure(d->GetHostName());
LoginFailure(d, "WRONGPWD");
sys_log(0, " WRONGPWD");
return;
}
if (auth.notAvailable)
{
LoginFailure(d, "NOTAVAIL");
sys_log(0, " NOTAVAIL");
return;
}
if (DESC_MANAGER::instance().FindByLoginName(login))
{
LoginFailure(d, "ALREADY");
sys_log(0, " ALREADY");
return;
}
if (strcmp(auth.status, "OK"))
{
LoginFailure(d, auth.status);
sys_log(0, " STATUS: %s", auth.status);
return;
}
if (LC_IsEurope())
{
if (strncmp(createDate, g_stBlockDate.c_str(), 8) >= 0)
{
LoginFailure(d, "BLKLOGIN");
sys_log(0, " BLKLOGIN");
return;
}
UpdateAccountLastPlay(auth.accountId);
}
TAccountTable& r = d->GetAccountTable();
int premiumTimes[PREMIUM_MAX_NUM];
r.id = auth.accountId;
trim_and_lower(login, r.login, sizeof(r.login));
strlcpy(r.passwd, passwd, sizeof(r.passwd));
strlcpy(r.social_id, auth.socialId, sizeof(r.social_id));
DESC_MANAGER::instance().ConnectAccount(r.login, d);
ClearLoginFailure(d->GetHostName());
thecore_memcpy(premiumTimes, auth.premiumTimes.data(), sizeof(premiumTimes));
DBManager::instance().LoginPrepare(d, premiumTimes);
sys_log(0, "%s: SUCCESS %s", logPrefix, login);
}
}
DBManager::DBManager() : m_bIsConnect(false)
{
}
@@ -232,6 +435,28 @@ void DBManager::LoginPrepare(LPDESC d, int * paiPremiumTimes)
SendAuthLogin(d);
}
void DBManager::AuthenticateLogin(LPDESC d, const char* login, const char* passwd)
{
AuthLoginData auth;
bool found = false;
d->SetLogin(login);
sys_log(0, "AUTH_LOGIN_DIRECT: START %u %p", d->GetLoginKey(), get_pointer(d));
if (IsChannelServiceLogin(login))
sys_log(0, "ChannelServiceLogin [%s]", login);
if (!LoadAuthLoginData(login, passwd, auth, found) || !found)
{
sys_log(0, " NOID");
RecordLoginFailure(d->GetHostName());
LoginFailure(d, "NOID");
return;
}
FinalizeAuthLogin(d, login, passwd, auth, "AUTH_LOGIN_DIRECT");
}
void DBManager::AnalyzeReturnQuery(SQLMsg * pMsg)
{
CReturnQueryInfo * qi = (CReturnQueryInfo *) pMsg->pvUserData;

View File

@@ -74,6 +74,7 @@ class DBManager : public singleton<DBManager>
std::unique_ptr<SQLMsg> DirectQuery(const char* c_pszFormat, ...);
void ReturnQuery(int iType, DWORD dwIdent, void* pvData, const char * c_pszFormat, ...);
CAsyncSQL* GetDirectSQL() { return &m_sql_direct; }
void Process();
void AnalyzeReturnQuery(SQLMsg * pmsg);
@@ -81,6 +82,7 @@ class DBManager : public singleton<DBManager>
void SendMoneyLog(BYTE type, DWORD vnum, int gold);
void LoginPrepare(LPDESC d, int * paiPremiumTimes = NULL);
void AuthenticateLogin(LPDESC d, const char* login, const char* passwd);
void SendAuthLogin(LPDESC d);
void SendLoginPing(const char * c_pszLogin);

View File

@@ -17,6 +17,18 @@
#include "locale_service.h"
#include "log.h"
namespace
{
bool IsPeerDisconnectWriteError(int error_code)
{
#ifdef OS_WINDOWS
return error_code == WSAECONNRESET || error_code == WSAECONNABORTED || error_code == WSAENOTCONN;
#else
return error_code == EPIPE || error_code == ECONNRESET || error_code == ENOTCONN;
#endif
}
}
extern int max_bytes_written;
extern int current_bytes_written;
extern int total_bytes_written;
@@ -277,22 +289,56 @@ int DESC::ProcessOutput()
if (bytes_to_write == 0)
return 0;
int result = socket_write(m_sock, (const char *) m_outputBuffer.ReadPtr(), bytes_to_write);
int bytes_written = send(m_sock, (const char *) m_outputBuffer.ReadPtr(), bytes_to_write, 0);
if (result == 0)
if (bytes_written > 0)
{
max_bytes_written = MAX(bytes_to_write, max_bytes_written);
max_bytes_written = MAX(bytes_written, max_bytes_written);
total_bytes_written += bytes_to_write;
current_bytes_written += bytes_to_write;
total_bytes_written += bytes_written;
current_bytes_written += bytes_written;
m_outputBuffer.Discard(bytes_to_write);
m_outputBuffer.Discard(bytes_written);
if (m_outputBuffer.ReadableBytes() != 0)
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
return 0;
}
return (result);
if (bytes_written == 0)
return -1;
#ifdef EINTR
if (errno == EINTR)
return 0;
#endif
#ifdef EAGAIN
if (errno == EAGAIN)
{
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
return 0;
}
#endif
#ifdef EWOULDBLOCK
if (errno == EWOULDBLOCK)
{
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
return 0;
}
#endif
const int error_code = errno;
BeginClosePhase();
if (IsPeerDisconnectWriteError(error_code))
sys_log(0, "ProcessOutput: peer disconnected during send (host=%s fd=%d errno=%d %s)", GetHostName(), m_sock, error_code, strerror(error_code));
else
sys_err("ProcessOutput: send failed (host=%s fd=%d errno=%d %s)", GetHostName(), m_sock, error_code, strerror(error_code));
return -1;
}
void DESC::BufferedPacket(const void * c_pvData, int iSize)
@@ -370,6 +416,9 @@ void DESC::Packet(const void * c_pvData, int iSize)
if (m_iPhase != PHASE_CLOSE)
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
if (m_iPhase != PHASE_CLOSE)
ProcessOutput();
}
void DESC::LargePacket(const void * c_pvData, int iSize)
@@ -378,8 +427,23 @@ void DESC::LargePacket(const void * c_pvData, int iSize)
Packet(c_pvData, iSize);
}
void DESC::BeginClosePhase()
{
if (m_iPhase == PHASE_CLOSE)
return;
m_iPhase = PHASE_CLOSE;
m_pInputProcessor = &m_inputClose;
}
void DESC::SetPhase(int _phase)
{
if (_phase == PHASE_CLOSE)
{
BeginClosePhase();
return;
}
m_iPhase = _phase;
TPacketGCPhase pack;
@@ -779,6 +843,9 @@ void DESC::RawPacket(const void * c_pvData, int iSize)
if (m_iPhase != PHASE_CLOSE)
fdwatch_add_fd(m_lpFdw, m_sock, this, FDW_WRITE, true);
if (m_iPhase != PHASE_CLOSE)
ProcessOutput();
}
void DESC::ChatPacket(BYTE type, const char * format, ...)

View File

@@ -146,6 +146,7 @@ class DESC
protected:
void Initialize();
void BeginClosePhase();
protected:
CInputProcessor * m_pInputProcessor;

View File

@@ -15,9 +15,30 @@
#include "locale_service.h"
#include "guild_manager.h"
#include "MarkManager.h"
#include "libsql/Statement.h"
namespace
{
bool CountGuildsByName(const char* guild_name, unsigned long long* count)
{
char query[256];
snprintf(query, sizeof(query), "SELECT COUNT(*) FROM guild%s WHERE name = ?", get_table_postfix());
CStmt stmt;
if (!stmt.Prepare(DBManager::instance().GetDirectSQL(), query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, (void*) guild_name, GUILD_NAME_MAX_LEN))
return false;
if (!stmt.BindResult(MYSQL_TYPE_LONGLONG, count))
return false;
if (!stmt.Execute() || !stmt.Fetch())
return false;
return true;
}
struct FGuildNameSender
{
@@ -81,25 +102,19 @@ DWORD CGuildManager::CreateGuild(TGuildCreateParameter& gcp)
return 0;
}
auto pmsg = DBManager::instance().DirectQuery("SELECT COUNT(*) FROM guild%s WHERE name = '%s'",
get_table_postfix(), gcp.name);
if (pmsg->Get()->uiNumRows > 0)
{
MYSQL_ROW row = mysql_fetch_row(pmsg->Get()->pSQLResult);
if (!(row[0] && row[0][0] == '0'))
{
gcp.master->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("<길드> 이미 같은 이름의 길드가 있습니다."));
return 0;
}
}
else
unsigned long long guild_count = 0;
if (!CountGuildsByName(gcp.name, &guild_count))
{
gcp.master->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("<길드> 길드를 생성할 수 없습니다."));
return 0;
}
if (guild_count != 0)
{
gcp.master->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("<길드> 이미 같은 이름의 길드가 있습니다."));
return 0;
}
// new CGuild(gcp) queries guild tables and tell dbcache to notice other game servers.
// other game server calls CGuildManager::LoadGuild to load guild.
CGuild * pg = M2_NEW CGuild(gcp);
@@ -956,4 +971,3 @@ void CGuildManager::ChangeMaster(DWORD dwGID)
"SELECT 1");
}

View File

@@ -245,7 +245,7 @@ int CInputHandshake::HandleText(LPDESC d, const char * c_pData)
stResult = "YES";
}
//else if (!stBuf.compare("SHOWMETHEMONEY"))
else if (stBuf == g_stAdminPagePassword)
else if (!g_stAdminPagePassword.empty() && stBuf == g_stAdminPagePassword)
{
if (!IsEmptyAdminPage())
{

View File

@@ -141,13 +141,6 @@ bool FN_IS_VALID_LOGIN_STRING(const char *str)
return true;
}
bool Login_IsInChannelService(const char* c_login)
{
if (c_login[0] == '[')
return true;
return false;
}
CInputAuth::CInputAuth()
{
RegisterHandlers();
@@ -233,50 +226,7 @@ void CInputAuth::Login(LPDESC d, const char * c_pData)
sys_log(0, "InputAuth::Login : key %u login %s", dwKey, login);
TPacketCGLogin3 * p = M2_NEW TPacketCGLogin3;
thecore_memcpy(p, pinfo, sizeof(TPacketCGLogin3));
char szPasswd[PASSWD_MAX_LEN * 2 + 1];
DBManager::instance().EscapeString(szPasswd, sizeof(szPasswd), passwd, strlen(passwd));
char szLogin[LOGIN_MAX_LEN * 2 + 1];
DBManager::instance().EscapeString(szLogin, sizeof(szLogin), login, strlen(login));
// CHANNEL_SERVICE_LOGIN
if (Login_IsInChannelService(szLogin))
{
sys_log(0, "ChannelServiceLogin [%s]", szLogin);
DBManager::instance().ReturnQuery(QID_AUTH_LOGIN, dwKey, p,
"SELECT '%s',password,social_id,id,status,availDt - NOW() > 0,"
"UNIX_TIMESTAMP(silver_expire),"
"UNIX_TIMESTAMP(gold_expire),"
"UNIX_TIMESTAMP(safebox_expire),"
"UNIX_TIMESTAMP(autoloot_expire),"
"UNIX_TIMESTAMP(fish_mind_expire),"
"UNIX_TIMESTAMP(marriage_fast_expire),"
"UNIX_TIMESTAMP(money_drop_rate_expire),"
"UNIX_TIMESTAMP(create_time)"
" FROM account WHERE login='%s'",
szPasswd, szLogin);
}
// END_OF_CHANNEL_SERVICE_LOGIN
else
{
DBManager::instance().ReturnQuery(QID_AUTH_LOGIN, dwKey, p,
"SELECT PASSWORD('%s'),password,social_id,id,status,availDt - NOW() > 0,"
"UNIX_TIMESTAMP(silver_expire),"
"UNIX_TIMESTAMP(gold_expire),"
"UNIX_TIMESTAMP(safebox_expire),"
"UNIX_TIMESTAMP(autoloot_expire),"
"UNIX_TIMESTAMP(fish_mind_expire),"
"UNIX_TIMESTAMP(marriage_fast_expire),"
"UNIX_TIMESTAMP(money_drop_rate_expire),"
"UNIX_TIMESTAMP(create_time)"
" FROM account WHERE login='%s'",
szPasswd, szLogin);
}
DBManager::instance().AuthenticateLogin(d, login, passwd);
}
int CInputAuth::Analyze(LPDESC d, uint16_t wHeader, const char * c_pData)

View File

@@ -167,6 +167,64 @@ void ShutdownOnFatalError()
namespace
{
const char* BoolState(bool value)
{
return value ? "on" : "off";
}
const char* EmptyToLabel(const char* value, const char* fallback)
{
return (value && *value) ? value : fallback;
}
const char* EmptyToLabel(const std::string& value, const char* fallback)
{
return value.empty() ? fallback : value.c_str();
}
void LogStartupSummary()
{
#ifdef ENABLE_PROXY_IP
const char* proxy_ip = EmptyToLabel(g_stProxyIP, "<disabled>");
#else
const char* proxy_ip = "<disabled>";
#endif
sys_log(0,
"[STARTUP] mode=%s channel=%u bind=%s:%u p2p=%s:%u db=%s:%u locale=%s quest_dir=%s",
g_bAuthServer ? "auth" : "game",
g_bChannel,
EmptyToLabel(g_szPublicIP, "0.0.0.0"), mother_port,
EmptyToLabel(g_szPublicIP, "0.0.0.0"), p2p_port,
EmptyToLabel(db_addr, "<unset>"), db_port,
EmptyToLabel(g_stLocale, "<unset>"),
EmptyToLabel(g_stQuestDir, "<unset>")
);
sys_log(0,
"[STARTUP] users limit=%d full=%d busy=%d local=%u p2p_peers=%d regen=%s admin_page=%s proxy=%s",
g_iUserLimit,
g_iFullUserCount,
g_iBusyUserCount,
DESC_MANAGER::instance().GetLocalUserCount(),
P2P_MANAGER::instance().GetDescCount(),
BoolState(!g_bNoRegen),
BoolState(!g_stAdminPagePassword.empty()),
proxy_ip
);
sys_log(0,
"[STARTUP] features client_version_check=%s guild_mark_server=%s mark_min_level=%u empire_whisper=%s auth_master=%s:%u test_server=%d",
BoolState(g_bCheckClientVersion),
BoolState(guild_mark_server),
guild_mark_min_level,
BoolState(g_bEmpireWhisper),
EmptyToLabel(g_stAuthMasterIP, "<disabled>"),
g_wAuthMasterPort,
test_server
);
}
struct SendDisconnectFunc
{
void operator () (LPDESC d)
@@ -344,14 +402,14 @@ int main(int argc, char **argv)
if (!start(argc, argv)) {
CleanUpForEarlyExit();
return 0;
return 1;
}
quest::CQuestManager quest_manager;
if (!quest_manager.Initialize()) {
CleanUpForEarlyExit();
return 0;
return 1;
}
MessengerManager::instance().Initialize();
@@ -428,7 +486,7 @@ int main(int argc, char **argv)
#endif
log_destroy();
return 1;
return 0;
}
void usage()
@@ -544,6 +602,16 @@ int start(int argc, char **argv)
main_fdw = fdwatch_new(4096);
if (!main_fdw)
{
sys_err("fdwatch_new failed");
return 0;
}
sys_log(0, "[STARTUP] fdwatch backend=%s descriptor_limit=%d",
fdwatch_backend_name(fdwatch_get_backend(main_fdw)),
fdwatch_get_descriptor_limit(main_fdw));
if ((tcp_socket = socket_tcp_bind(g_szPublicIP, mother_port)) == INVALID_SOCKET)
{
perror("socket_tcp_bind: tcp_socket");
@@ -594,6 +662,8 @@ int start(int argc, char **argv)
LoadSpamDB();
}
LogStartupSummary();
signal_timer_enable(30);
return 1;
}
@@ -778,4 +848,3 @@ int io_loop(LPFDWATCH fdw)
return 1;
}

80
src/game/quest_packet.cpp Normal file
View File

@@ -0,0 +1,80 @@
#include "stdafx.h"
#include "common/tables.h"
#include "packet_structs.h"
#include "quest_packet.h"
#include <algorithm>
#include <array>
#include <cstring>
namespace quest
{
namespace
{
constexpr uint8_t QUEST_SEND_ISBEGIN_LOCAL = 1 << 0;
constexpr uint8_t QUEST_SEND_TITLE_LOCAL = 1 << 1;
constexpr uint8_t QUEST_SEND_CLOCK_NAME_LOCAL = 1 << 2;
constexpr uint8_t QUEST_SEND_CLOCK_VALUE_LOCAL = 1 << 3;
constexpr uint8_t QUEST_SEND_COUNTER_NAME_LOCAL = 1 << 4;
constexpr uint8_t QUEST_SEND_COUNTER_VALUE_LOCAL = 1 << 5;
constexpr uint8_t QUEST_SEND_ICON_FILE_LOCAL = 1 << 6;
void AppendBytes(std::vector<uint8_t>& packet, const void* data, size_t size)
{
const auto* bytes = static_cast<const uint8_t*>(data);
packet.insert(packet.end(), bytes, bytes + size);
}
template <size_t N>
void AppendFixedString(std::vector<uint8_t>& packet, const std::string& value)
{
std::array<char, N> field {};
const size_t copy_size = std::min(value.size(), N - 1);
if (copy_size > 0)
std::memcpy(field.data(), value.data(), copy_size);
AppendBytes(packet, field.data(), field.size());
}
}
std::vector<uint8_t> BuildQuestInfoPacket(const QuestInfoPacketData& data)
{
packet_quest_info header {};
header.header = GC::QUEST_INFO;
header.length = sizeof(header);
header.index = data.quest_index;
header.flag = data.send_flags;
std::vector<uint8_t> packet;
packet.reserve(sizeof(header) + 128);
AppendBytes(packet, &header, sizeof(header));
if (data.send_flags & QUEST_SEND_ISBEGIN_LOCAL)
{
const uint8_t is_begin = data.is_begin ? 1 : 0;
AppendBytes(packet, &is_begin, sizeof(is_begin));
}
if (data.send_flags & QUEST_SEND_TITLE_LOCAL)
AppendFixedString<30 + 1>(packet, data.title);
if (data.send_flags & QUEST_SEND_CLOCK_NAME_LOCAL)
AppendFixedString<16 + 1>(packet, data.clock_name);
if (data.send_flags & QUEST_SEND_CLOCK_VALUE_LOCAL)
AppendBytes(packet, &data.clock_value, sizeof(data.clock_value));
if (data.send_flags & QUEST_SEND_COUNTER_NAME_LOCAL)
AppendFixedString<16 + 1>(packet, data.counter_name);
if (data.send_flags & QUEST_SEND_COUNTER_VALUE_LOCAL)
AppendBytes(packet, &data.counter_value, sizeof(data.counter_value));
if (data.send_flags & QUEST_SEND_ICON_FILE_LOCAL)
AppendFixedString<24 + 1>(packet, data.icon_file);
auto* final_header = reinterpret_cast<packet_quest_info*>(packet.data());
final_header->length = static_cast<uint16_t>(packet.size());
return packet;
}
}

23
src/game/quest_packet.h Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace quest
{
struct QuestInfoPacketData
{
uint16_t quest_index = 0;
uint8_t send_flags = 0;
bool is_begin = false;
std::string title;
std::string clock_name;
int clock_value = 0;
std::string counter_name;
int counter_value = 0;
std::string icon_file;
};
std::vector<uint8_t> BuildQuestInfoPacket(const QuestInfoPacketData& data);
}

View File

@@ -22,6 +22,7 @@
#include "utils.h"
#include "unique_item.h"
#include "mob_manager.h"
#include "libsql/Statement.h"
#include <cctype>
#undef sys_err
@@ -33,6 +34,48 @@ const int ITEM_BROKEN_METIN_VNUM = 28960;
namespace quest
{
namespace
{
bool CountPlayersByName(const char* player_name, unsigned long long* count)
{
char query[256];
snprintf(query, sizeof(query), "SELECT COUNT(*) FROM player%s WHERE name = ?", get_table_postfix());
CStmt stmt;
if (!stmt.Prepare(DBManager::instance().GetDirectSQL(), query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, (void*) player_name, CHARACTER_NAME_MAX_LEN))
return false;
if (!stmt.BindResult(MYSQL_TYPE_LONGLONG, count))
return false;
if (!stmt.Execute() || !stmt.Fetch())
return false;
return true;
}
bool UpdatePlayerName(DWORD player_id, const char* player_name)
{
char query[256];
snprintf(query, sizeof(query), "UPDATE player%s SET name = ?, change_name = 0 WHERE id = ?", get_table_postfix());
CStmt stmt;
if (!stmt.Prepare(DBManager::instance().GetDirectSQL(), query))
return false;
if (!stmt.BindParam(MYSQL_TYPE_STRING, (void*) player_name, CHARACTER_NAME_MAX_LEN))
return false;
if (!stmt.BindParam(MYSQL_TYPE_LONG, &player_id))
return false;
return stmt.Execute() != 0;
}
}
//
// "pc" Lua functions
//
@@ -2114,23 +2157,18 @@ teleport_area:
return 1;
}
char szQuery[1024];
snprintf(szQuery, sizeof(szQuery), "SELECT COUNT(*) FROM player%s WHERE name='%s'", get_table_postfix(), szName);
auto pmsg = DBManager::instance().DirectQuery(szQuery);
if ( pmsg->Get()->uiNumRows > 0 )
unsigned long long count = 0;
if (!CountPlayersByName(szName, &count))
{
MYSQL_ROW row = mysql_fetch_row(pmsg->Get()->pSQLResult);
lua_pushnumber(L, 5);
return 1;
}
int count = 0;
str_to_number(count, row[0]);
// 이미 해당 이름을 가진 캐릭터가 있음
if ( count != 0 )
{
lua_pushnumber(L, 3);
return 1;
}
// 이미 해당 이름을 가진 캐릭터가 있음
if (count != 0)
{
lua_pushnumber(L, 3);
return 1;
}
DWORD pid = ch->GetPlayerID();
@@ -2144,8 +2182,11 @@ teleport_area:
/* change_name_log */
LogManager::instance().ChangeNameLog(pid, ch->GetName(), szName, ch->GetDesc()->GetHostName());
snprintf(szQuery, sizeof(szQuery), "UPDATE player%s SET name='%s' WHERE id=%u", get_table_postfix(), szName, pid);
DBManager::instance().DirectQuery(szQuery);
if (!UpdatePlayerName(pid, szName))
{
lua_pushnumber(L, 5);
return 1;
}
ch->SetNewName(szName);
lua_pushnumber(L, 4);

View File

@@ -1,8 +1,8 @@
#include "stdafx.h"
#include "constants.h"
#include "quest_packet.h"
#include "questmanager.h"
#include "packet_structs.h"
#include "buffer_manager.h"
#include "char.h"
#include "desc_client.h"
#include "questevent.h"
@@ -234,72 +234,34 @@ namespace quest
assert(m_iSendToClient);
assert(m_RunningQuestState);
packet_quest_info qi;
qi.header = GC::QUEST_INFO;
qi.length = sizeof(struct packet_quest_info);
qi.index = m_RunningQuestState->iIndex;
qi.flag = m_iSendToClient;
TEMP_BUFFER buf;
buf.write(&qi, sizeof(qi));
QuestInfoPacketData packet_data {};
packet_data.quest_index = static_cast<uint16_t>(m_RunningQuestState->iIndex);
packet_data.send_flags = static_cast<uint8_t>(m_iSendToClient);
packet_data.is_begin = m_RunningQuestState->bStart;
packet_data.title = m_RunningQuestState->_title;
packet_data.clock_name = m_RunningQuestState->_clock_name;
packet_data.clock_value = m_RunningQuestState->_clock_value;
packet_data.counter_name = m_RunningQuestState->_counter_name;
packet_data.counter_value = m_RunningQuestState->_counter_value;
packet_data.icon_file = m_RunningQuestState->_icon_file;
if (m_iSendToClient & QUEST_SEND_ISBEGIN)
{
BYTE temp = m_RunningQuestState->bStart?1:0;
buf.write(&temp,1);
qi.length+=1;
sys_log(1, "QUEST BeginFlag %d", (int)temp);
}
sys_log(1, "QUEST BeginFlag %d", static_cast<int>(m_RunningQuestState->bStart ? 1 : 0));
if (m_iSendToClient & QUEST_SEND_TITLE)
{
m_RunningQuestState->_title.reserve(30+1);
buf.write(m_RunningQuestState->_title.c_str(), 30+1);
qi.length+=30+1;
sys_log(1, "QUEST Title %s", m_RunningQuestState->_title.c_str());
}
if (m_iSendToClient & QUEST_SEND_CLOCK_NAME)
{
m_RunningQuestState->_clock_name.reserve(16+1);
buf.write(m_RunningQuestState->_clock_name.c_str(), 16+1);
qi.length+=16+1;
sys_log(1, "QUEST Clock Name %s", m_RunningQuestState->_clock_name.c_str());
}
if (m_iSendToClient & QUEST_SEND_CLOCK_VALUE)
{
buf.write(&m_RunningQuestState->_clock_value, sizeof(int));
qi.length+=4;
sys_log(1, "QUEST Clock Value %d", m_RunningQuestState->_clock_value);
}
if (m_iSendToClient & QUEST_SEND_COUNTER_NAME)
{
m_RunningQuestState->_counter_name.reserve(16+1);
buf.write(m_RunningQuestState->_counter_name.c_str(), 16+1);
qi.length+=16+1;
sys_log(1, "QUEST Counter Name %s", m_RunningQuestState->_counter_name.c_str());
}
if (m_iSendToClient & QUEST_SEND_COUNTER_VALUE)
{
buf.write(&m_RunningQuestState->_counter_value, sizeof(int));
qi.length+=4;
sys_log(1, "QUEST Counter Value %d", m_RunningQuestState->_counter_value);
}
if (m_iSendToClient & QUEST_SEND_ICON_FILE)
{
m_RunningQuestState->_icon_file.reserve(24+1);
buf.write(m_RunningQuestState->_icon_file.c_str(), 24+1);
qi.length+=24+1;
sys_log(1, "QUEST Icon File %s", m_RunningQuestState->_icon_file.c_str());
}
CQuestManager::instance().GetCurrentCharacterPtr()->GetDesc()->Packet(buf.read_peek(),buf.size());
auto packet = BuildQuestInfoPacket(packet_data);
CQuestManager::instance().GetCurrentCharacterPtr()->GetDesc()->Packet(packet.data(), packet.size());
m_iSendToClient = 0;
@@ -722,4 +684,3 @@ namespace quest
}
}
}

View File

@@ -0,0 +1,6 @@
#pragma once
inline bool HasRecentRequestCooldown(int last_request_pulse, int current_pulse, int cooldown_pulses)
{
return last_request_pulse > 0 && current_pulse - last_request_pulse < cooldown_pulses;
}

View File

@@ -46,10 +46,14 @@ bool CAsyncSQL::QueryLocaleSet()
return true;
}
const char* current_charset = mysql_character_set_name(&m_hDB);
if (current_charset && m_stLocale == current_charset)
return true;
if (mysql_set_character_set(&m_hDB, m_stLocale.c_str()))
{
sys_err("cannot set locale %s by 'mysql_set_character_set', errno %u %s",
m_stLocale.c_str(), mysql_errno(&m_hDB), mysql_error(&m_hDB));
sys_err("cannot set locale %s by 'mysql_set_character_set' (current=%s), errno %u %s",
m_stLocale.c_str(), current_charset ? current_charset : "<unknown>", mysql_errno(&m_hDB), mysql_error(&m_hDB));
return false;
}
@@ -138,6 +142,16 @@ bool CAsyncSQL::Setup(const char* c_pszHost, const char* c_pszUser, const char*
void CAsyncSQL::Quit()
{
sys_log(0,
"[SHUTDOWN] AsyncSQL quit begin db=%s host=%s worker=%s pending=%u copied=%u results=%u connected=%d",
m_stDB.c_str(),
m_stHost.c_str(),
HasWorkerThread() ? "yes" : "no",
CountQuery(),
CountCopiedQueryQueue(),
CountResult(),
IsConnected() ? 1 : 0);
m_bEnd.store(true, std::memory_order_release);
m_cvQuery.notify_all();
@@ -146,6 +160,14 @@ void CAsyncSQL::Quit()
m_thread->join();
m_thread.reset();
}
sys_log(0,
"[SHUTDOWN] AsyncSQL quit done db=%s host=%s pending=%u copied=%u results=%u",
m_stDB.c_str(),
m_stHost.c_str(),
CountQuery(),
CountCopiedQueryQueue(),
CountResult());
}
std::unique_ptr<SQLMsg> CAsyncSQL::DirectQuery(const char* c_pszQuery)
@@ -323,6 +345,17 @@ DWORD CAsyncSQL::CountResult()
return static_cast<DWORD>(m_queue_result.size());
}
DWORD CAsyncSQL::CountCopiedQueryQueue()
{
std::lock_guard<std::mutex> lock(m_mtxQuery);
return static_cast<DWORD>(m_queue_query_copy.size());
}
bool CAsyncSQL::HasWorkerThread() const
{
return m_thread && m_thread->joinable();
}
// Modern profiler using chrono
class cProfiler
{
@@ -528,6 +561,15 @@ void CAsyncSQL::ChildLoop()
m_iQueryFinished.fetch_add(1, std::memory_order_acq_rel);
}
}
sys_log(0,
"[SHUTDOWN] AsyncSQL worker exit db=%s host=%s pending=%u copied=%u results=%u finished=%d",
m_stDB.c_str(),
m_stHost.c_str(),
CountQuery(),
CountCopiedQueryQueue(),
CountResult(),
CountQueryFinished());
}
int CAsyncSQL::CountQueryFinished() const
@@ -576,5 +618,7 @@ size_t CAsyncSQL::EscapeString(char* dst, size_t dstSize, const char* src, size_
void CAsyncSQL2::SetLocale(const std::string& stLocale)
{
m_stLocale = stLocale;
QueryLocaleSet();
if (!HasWorkerThread())
QueryLocaleSet();
}

View File

@@ -175,6 +175,8 @@ class CAsyncSQL
DWORD CountQuery();
DWORD CountResult();
DWORD CountCopiedQueryQueue();
bool HasWorkerThread() const;
void PushResult(std::unique_ptr<SQLMsg> p);
bool PopResult(std::unique_ptr<SQLMsg>& p);

View File

@@ -9,7 +9,6 @@ CStmt::CStmt()
m_uiParamCount = 0;
m_uiResultCount = 0;
iRows = 0;
m_puiParamLen = NULL;
}
CStmt::~CStmt()
@@ -25,11 +24,13 @@ void CStmt::Destroy()
m_pkStmt = NULL;
}
if (m_puiParamLen)
{
free(m_puiParamLen);
m_puiParamLen = NULL;
}
m_vec_param.clear();
m_vecParamLen.clear();
m_vec_result.clear();
m_vecResultLen.clear();
m_uiParamCount = 0;
m_uiResultCount = 0;
iRows = 0;
}
void CStmt::Error(const char * c_pszMsg)
@@ -39,6 +40,7 @@ void CStmt::Error(const char * c_pszMsg)
bool CStmt::Prepare(CAsyncSQL * sql, const char * c_pszQuery)
{
Destroy();
m_pkStmt = mysql_stmt_init(sql->GetSQLHandle());
m_stQuery = c_pszQuery;
@@ -48,27 +50,20 @@ bool CStmt::Prepare(CAsyncSQL * sql, const char * c_pszQuery)
return false;
}
int iParamCount = 0;
for (unsigned int i = 0; i < m_stQuery.length(); ++i)
if (c_pszQuery[i] == '?')
++iParamCount;
if (iParamCount)
const unsigned int param_count = mysql_stmt_param_count(m_pkStmt);
if (param_count)
{
m_vec_param.resize(iParamCount);
memset(&m_vec_param[0], 0, sizeof(MYSQL_BIND) * iParamCount);
m_puiParamLen = (long unsigned int *) calloc(iParamCount, sizeof(long unsigned int));
m_vec_param.resize(param_count);
memset(&m_vec_param[0], 0, sizeof(MYSQL_BIND) * param_count);
m_vecParamLen.resize(param_count, 0);
}
m_vec_result.resize(48);
memset(&m_vec_result[0], 0, sizeof(MYSQL_BIND) * 48);
if (mysql_stmt_bind_result(m_pkStmt, &m_vec_result[0]))
const unsigned int result_count = mysql_stmt_field_count(m_pkStmt);
if (result_count)
{
Error("mysql_stmt_bind_result");
return 0;
m_vec_result.resize(result_count);
memset(&m_vec_result[0], 0, sizeof(MYSQL_BIND) * result_count);
m_vecResultLen.resize(result_count, 0);
}
return true;
@@ -87,16 +82,8 @@ bool CStmt::BindParam(enum_field_types type, void * p, int iMaxLen)
bind->buffer_type = type;
bind->buffer = (void *) p;
bind->buffer_length = iMaxLen;
bind->length = m_puiParamLen + m_uiParamCount;
if (++m_uiParamCount == m_vec_param.size())
{
if (mysql_stmt_bind_param(m_pkStmt, &m_vec_param[0]))
{
Error("mysql_stmt_bind_param");
return false;
}
}
bind->length = m_vecParamLen.empty() ? NULL : &m_vecParamLen[m_uiParamCount];
++m_uiParamCount;
return true;
}
@@ -114,6 +101,7 @@ bool CStmt::BindResult(enum_field_types type, void * p, int iMaxLen)
bind->buffer_type = type;
bind->buffer = (void *) p;
bind->buffer_length = iMaxLen;
bind->length = m_vecResultLen.empty() ? NULL : &m_vecResultLen[m_uiResultCount - 1];
return true;
}
@@ -131,9 +119,18 @@ int CStmt::Execute()
if (bind->buffer_type == MYSQL_TYPE_STRING)
{
*(m_puiParamLen + i) = strlen((const char *) bind->buffer);
sys_log(0, "param %d len %d buf %s", i, *m_puiParamLen, (const char *) bind->buffer);
m_vecParamLen[i] = strlen((const char *) bind->buffer);
}
else if (bind->buffer_type == MYSQL_TYPE_BLOB)
{
m_vecParamLen[i] = bind->buffer_length;
}
}
if (!m_vec_param.empty() && mysql_stmt_bind_param(m_pkStmt, &m_vec_param[0]))
{
Error("mysql_stmt_bind_param");
return 0;
}
if (mysql_stmt_execute(m_pkStmt))
@@ -142,13 +139,31 @@ int CStmt::Execute()
return 0;
}
if (mysql_stmt_store_result(m_pkStmt))
if (!m_vec_result.empty())
{
Error("mysql_store_result");
return 0;
if (m_uiResultCount != m_vec_result.size())
{
sys_log(0, "Result count mismatch %u, expected %zu query: %s", m_uiResultCount, m_vec_result.size(), m_stQuery.c_str());
return 0;
}
if (mysql_stmt_bind_result(m_pkStmt, &m_vec_result[0]))
{
Error("mysql_stmt_bind_result");
return 0;
}
if (mysql_stmt_store_result(m_pkStmt))
{
Error("mysql_stmt_store_result");
return 0;
}
iRows = mysql_stmt_num_rows(m_pkStmt);
return true;
}
iRows = mysql_stmt_num_rows(m_pkStmt);
iRows = 0;
return true;
}
@@ -157,3 +172,18 @@ bool CStmt::Fetch()
return !mysql_stmt_fetch(m_pkStmt);
}
unsigned long long CStmt::GetInsertId() const
{
if (!m_pkStmt)
return 0;
return mysql_stmt_insert_id(m_pkStmt);
}
unsigned long long CStmt::GetAffectedRows() const
{
if (!m_pkStmt)
return 0;
return mysql_stmt_affected_rows(m_pkStmt);
}

View File

@@ -17,6 +17,8 @@ class CStmt
bool BindResult(enum_field_types type, void * p, int iMaxLen=0);
int Execute();
bool Fetch();
unsigned long long GetInsertId() const;
unsigned long long GetAffectedRows() const;
void Error(const char * c_pszMsg);
@@ -32,10 +34,11 @@ class CStmt
std::vector<MYSQL_BIND> m_vec_param;
unsigned int m_uiParamCount;
long unsigned int * m_puiParamLen;
std::vector<unsigned long> m_vecParamLen;
std::vector<MYSQL_BIND> m_vec_result;
unsigned int m_uiResultCount;
std::vector<unsigned long> m_vecResultLen;
};
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,36 @@
#pragma once
#ifndef __USE_SELECT__
typedef struct fdwatch FDWATCH;
typedef struct fdwatch * LPFDWATCH;
typedef struct fdwatch FDWATCH;
typedef struct fdwatch * LPFDWATCH;
enum EFdwatch
{
FDW_NONE = 0,
FDW_READ = 1,
FDW_WRITE = 2,
FDW_WRITE_ONESHOT = 4,
FDW_EOF = 8,
};
enum EFdwatch
{
FDW_NONE = 0,
FDW_READ = 1,
FDW_WRITE = 2,
FDW_WRITE_ONESHOT = 4,
FDW_EOF = 8,
};
typedef struct kevent KEVENT;
typedef struct kevent * LPKEVENT;
typedef int KQUEUE;
struct fdwatch
{
KQUEUE kq;
int nfiles;
LPKEVENT kqevents;
int nkqevents;
LPKEVENT kqrevents;
int * fd_event_idx;
void ** fd_data;
int * fd_rw;
};
#else
typedef struct fdwatch FDWATCH;
typedef struct fdwatch * LPFDWATCH;
enum EFdwatch
{
FDW_NONE = 0,
FDW_READ = 1,
FDW_WRITE = 2,
FDW_WRITE_ONESHOT = 4,
FDW_EOF = 8,
};
struct fdwatch
{
fd_set rfd_set;
fd_set wfd_set;
socket_t* select_fds;
int* select_rfdidx;
int nselect_fds;
fd_set working_rfd_set;
fd_set working_wfd_set;
int nfiles;
void** fd_data;
int* fd_rw;
};
#endif // WIN32
LPFDWATCH fdwatch_new(int nfiles);
void fdwatch_clear_fd(LPFDWATCH fdw, socket_t fd);
void fdwatch_delete(LPFDWATCH fdw);
int fdwatch_check_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_check_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_clear_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_add_fd(LPFDWATCH fdw, socket_t fd, void* client_data, int rw, int oneshot);
int fdwatch(LPFDWATCH fdw, struct timeval *timeout);
void * fdwatch_get_client_data(LPFDWATCH fdw, unsigned int event_idx);
void fdwatch_del_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_buffer_size(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_ident(LPFDWATCH fdw, unsigned int event_idx);
enum EFdwatchBackend
{
FDWATCH_BACKEND_KQUEUE = 0,
FDWATCH_BACKEND_SELECT = 1,
FDWATCH_BACKEND_EPOLL = 2,
};
LPFDWATCH fdwatch_new(int nfiles);
void fdwatch_clear_fd(LPFDWATCH fdw, socket_t fd);
void fdwatch_delete(LPFDWATCH fdw);
int fdwatch_check_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_check_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_clear_event(LPFDWATCH fdw, socket_t fd, unsigned int event_idx);
void fdwatch_add_fd(LPFDWATCH fdw, socket_t fd, void* client_data, int rw, int oneshot);
int fdwatch(LPFDWATCH fdw, struct timeval *timeout);
void * fdwatch_get_client_data(LPFDWATCH fdw, unsigned int event_idx);
void fdwatch_del_fd(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_buffer_size(LPFDWATCH fdw, socket_t fd);
int fdwatch_get_ident(LPFDWATCH fdw, unsigned int event_idx);
EFdwatchBackend fdwatch_get_backend(LPFDWATCH fdw);
const char * fdwatch_backend_name(EFdwatchBackend backend);
int fdwatch_get_descriptor_limit(LPFDWATCH fdw);

View File

@@ -6,7 +6,6 @@
LPHEART thecore_heart = NULL;
std::atomic<int> shutdowned = FALSE;
std::atomic<int> tics = 0;
unsigned int thecore_profiler[NUM_PF];
static int pid_init(void)
@@ -19,12 +18,12 @@ static int pid_init(void)
{
fprintf(fp, "%d", getpid());
fclose(fp);
sys_err("\nStart of pid: %d\n", getpid());
sys_log(0, "Start of pid: %d", getpid());
}
else
{
printf("pid_init(): could not open file for writing. (filename: ./pid)");
sys_err("\nError writing pid file\n");
sys_err("Error writing pid file");
return false;
}
return true;
@@ -37,7 +36,7 @@ static void pid_deinit(void)
return;
#else
remove("./pid");
sys_err("\nEnd of pid\n");
sys_log(0, "End of pid");
#endif
}
@@ -89,6 +88,7 @@ int thecore_idle(void)
void thecore_destroy(void)
{
signal_destroy();
pid_deinit();
log_destroy();
}
@@ -115,5 +115,5 @@ int thecore_is_shutdowned(void)
void thecore_tick(void)
{
++tics;
signal_mark_progress();
}

View File

@@ -2,7 +2,6 @@
#include <atomic>
extern std::atomic<int> tics;
extern std::atomic<int> shutdowned;
#include "heart.h"
@@ -27,5 +26,4 @@ float thecore_time(void);
float thecore_pulse_per_second(void);
int thecore_is_shutdowned(void);
void thecore_tick(void); // tics 증가
void thecore_tick(void); // checkpoint progress 증가

View File

@@ -1,9 +1,125 @@
#include "stdafx.h"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
namespace
{
std::atomic<uint64_t> s_checkpoint_progress { 0 };
#ifndef OS_WINDOWS
std::mutex s_checkpoint_mutex;
std::condition_variable s_checkpoint_cv;
std::thread s_checkpoint_thread;
bool s_checkpoint_shutdown = false;
bool s_checkpoint_enabled = false;
int s_checkpoint_timeout_seconds = 0;
uint64_t s_checkpoint_generation = 0;
void checkpoint_watchdog_loop()
{
uint64_t last_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
auto last_change = std::chrono::steady_clock::now();
uint64_t observed_generation = 0;
std::unique_lock<std::mutex> lock(s_checkpoint_mutex);
while (!s_checkpoint_shutdown)
{
if (!s_checkpoint_enabled || s_checkpoint_timeout_seconds <= 0)
{
s_checkpoint_cv.wait(lock, []()
{
return s_checkpoint_shutdown || (s_checkpoint_enabled && s_checkpoint_timeout_seconds > 0);
});
last_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
last_change = std::chrono::steady_clock::now();
observed_generation = s_checkpoint_generation;
continue;
}
const int timeout_seconds = s_checkpoint_timeout_seconds;
const uint64_t generation = s_checkpoint_generation;
const auto poll_interval = std::chrono::seconds(1);
const bool reconfigured = s_checkpoint_cv.wait_for(lock, poll_interval, [generation]()
{
return s_checkpoint_shutdown || s_checkpoint_generation != generation;
});
if (s_checkpoint_shutdown)
break;
if (reconfigured || observed_generation != s_checkpoint_generation)
{
last_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
last_change = std::chrono::steady_clock::now();
observed_generation = s_checkpoint_generation;
continue;
}
const uint64_t current_progress = s_checkpoint_progress.load(std::memory_order_relaxed);
const auto now = std::chrono::steady_clock::now();
if (current_progress != last_progress)
{
last_progress = current_progress;
last_change = now;
continue;
}
if (now - last_change >= std::chrono::seconds(timeout_seconds))
{
lock.unlock();
sys_err("CHECKPOINT shutdown: no progress observed for %d seconds.", timeout_seconds);
abort();
}
}
}
void checkpoint_ensure_started()
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
if (s_checkpoint_thread.joinable())
return;
s_checkpoint_shutdown = false;
s_checkpoint_enabled = false;
s_checkpoint_timeout_seconds = 0;
s_checkpoint_generation = 0;
s_checkpoint_thread = std::thread(checkpoint_watchdog_loop);
}
#endif
const char* signal_checkpoint_backend_name_impl(ECheckpointBackend backend)
{
switch (backend)
{
case CHECKPOINT_BACKEND_NONE:
return "none";
case CHECKPOINT_BACKEND_VIRTUAL_TIMER:
return "virtual-timer";
case CHECKPOINT_BACKEND_WATCHDOG_THREAD:
return "watchdog-thread";
default:
return "unknown";
}
}
}
#ifdef OS_WINDOWS
void signal_setup() {}
void signal_destroy() {}
void signal_timer_disable() {}
void signal_timer_enable(int timeout_seconds) {}
void signal_mark_progress() {}
ECheckpointBackend signal_checkpoint_backend() { return CHECKPOINT_BACKEND_NONE; }
const char* signal_checkpoint_backend_name(ECheckpointBackend backend) { return signal_checkpoint_backend_name_impl(backend); }
#else
#define RETSIGTYPE void
@@ -14,18 +130,6 @@ RETSIGTYPE reap(int sig)
}
RETSIGTYPE checkpointing(int sig)
{
if (!tics.load())
{
sys_err("CHECKPOINT shutdown: tics did not updated.");
abort();
}
else
tics.store(0);
}
RETSIGTYPE hupsig(int sig)
{
shutdowned = TRUE;
@@ -39,46 +143,84 @@ RETSIGTYPE usrsig(int sig)
void signal_timer_disable(void)
{
struct itimerval itime;
struct timeval interval;
checkpoint_ensure_started();
interval.tv_sec = 0;
interval.tv_usec = 0;
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
s_checkpoint_enabled = false;
s_checkpoint_timeout_seconds = 0;
++s_checkpoint_generation;
}
itime.it_interval = interval;
itime.it_value = interval;
setitimer(ITIMER_VIRTUAL, &itime, NULL);
s_checkpoint_cv.notify_all();
}
void signal_timer_enable(int sec)
{
struct itimerval itime;
struct timeval interval;
checkpoint_ensure_started();
interval.tv_sec = sec;
interval.tv_usec = 0;
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
s_checkpoint_enabled = sec > 0;
s_checkpoint_timeout_seconds = sec;
++s_checkpoint_generation;
}
itime.it_interval = interval;
itime.it_value = interval;
setitimer(ITIMER_VIRTUAL, &itime, NULL);
s_checkpoint_cv.notify_all();
}
void signal_setup(void)
{
signal_timer_enable(30);
checkpoint_ensure_started();
signal_timer_enable(30);
signal(SIGVTALRM, checkpointing);
/* just to be on the safe side: */
signal(SIGHUP, hupsig);
signal(SIGCHLD, reap);
signal(SIGINT, hupsig);
signal(SIGTERM, hupsig);
signal(SIGPIPE, SIG_IGN);
signal(SIGALRM, SIG_IGN);
signal(SIGUSR1, usrsig);
/* just to be on the safe side: */
signal(SIGHUP, hupsig);
signal(SIGCHLD, reap);
signal(SIGINT, hupsig);
signal(SIGTERM, hupsig);
signal(SIGPIPE, SIG_IGN);
signal(SIGALRM, SIG_IGN);
signal(SIGUSR1, usrsig);
sys_log(0, "[STARTUP] checkpoint backend=%s", signal_checkpoint_backend_name(signal_checkpoint_backend()));
}
void signal_destroy()
{
std::thread checkpoint_thread;
{
std::lock_guard<std::mutex> lock(s_checkpoint_mutex);
if (!s_checkpoint_thread.joinable())
return;
s_checkpoint_shutdown = true;
s_checkpoint_enabled = false;
s_checkpoint_timeout_seconds = 0;
++s_checkpoint_generation;
checkpoint_thread = std::move(s_checkpoint_thread);
}
s_checkpoint_cv.notify_all();
checkpoint_thread.join();
s_checkpoint_progress.store(0, std::memory_order_relaxed);
}
void signal_mark_progress()
{
s_checkpoint_progress.fetch_add(1, std::memory_order_relaxed);
}
ECheckpointBackend signal_checkpoint_backend()
{
return CHECKPOINT_BACKEND_WATCHDOG_THREAD;
}
const char* signal_checkpoint_backend_name(ECheckpointBackend backend)
{
return signal_checkpoint_backend_name_impl(backend);
}
#endif

View File

@@ -1,4 +1,16 @@
#pragma once
enum ECheckpointBackend
{
CHECKPOINT_BACKEND_NONE = 0,
CHECKPOINT_BACKEND_VIRTUAL_TIMER = 1,
CHECKPOINT_BACKEND_WATCHDOG_THREAD = 2,
};
void signal_setup();
void signal_destroy();
void signal_timer_disable();
void signal_timer_enable(int timeout_seconds);
void signal_timer_enable(int timeout_seconds);
void signal_mark_progress();
ECheckpointBackend signal_checkpoint_backend();
const char* signal_checkpoint_backend_name(ECheckpointBackend backend);

View File

@@ -10,6 +10,31 @@ void socket_timeout(socket_t s, long sec, long usec);
void socket_reuse(socket_t s);
void socket_keepalive(socket_t s);
namespace
{
bool socket_accept_should_retry()
{
#ifdef OS_WINDOWS
const int wsa_error = WSAGetLastError();
return wsa_error == WSAEWOULDBLOCK || wsa_error == WSAEINTR;
#else
#ifdef EINTR
if (errno == EINTR)
return true;
#endif
#ifdef EAGAIN
if (errno == EAGAIN)
return true;
#endif
#ifdef EWOULDBLOCK
if (errno == EWOULDBLOCK)
return true;
#endif
return false;
#endif
}
}
int socket_read(socket_t desc, char* read_point, size_t space_left)
{
int ret;
@@ -20,7 +45,10 @@ int socket_read(socket_t desc, char* read_point, size_t space_left)
return ret;
if (ret == 0) // 정상적으로 접속 끊김
{
errno = 0;
return -1;
}
#ifdef EINTR /* Interrupted system call - various platforms */
if (errno == EINTR)
@@ -206,6 +234,8 @@ socket_t socket_accept(socket_t s, struct sockaddr_in *peer)
if ((desc = accept(s, (struct sockaddr *) peer, &i)) == -1)
{
if (socket_accept_should_retry())
return -1;
sys_err("accept: %s (fd %d)", strerror(errno), s);
return -1;
}

View File

@@ -102,6 +102,8 @@ inline double rint(double x)
#ifdef OS_FREEBSD
#include <sys/event.h>
#elif defined(__linux__)
#include <sys/epoll.h>
#endif
#endif

33
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,33 @@
if(WIN32)
return()
endif()
add_executable(metin_smoke_tests
smoke_auth.cpp
${CMAKE_SOURCE_DIR}/src/game/SecureCipher.cpp
${CMAKE_SOURCE_DIR}/src/game/quest_packet.cpp
)
add_executable(metin_login_smoke
login_smoke.cpp
${CMAKE_SOURCE_DIR}/src/game/SecureCipher.cpp
)
target_link_libraries(metin_smoke_tests
libthecore
sodium
pthread
)
target_link_libraries(metin_login_smoke
libthecore
sodium
pthread
)
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
target_link_libraries(metin_smoke_tests md)
target_link_libraries(metin_login_smoke md)
endif()
add_test(NAME metin_smoke_tests COMMAND metin_smoke_tests)

1228
tests/login_smoke.cpp Normal file

File diff suppressed because it is too large Load Diff

446
tests/smoke_auth.cpp Normal file
View File

@@ -0,0 +1,446 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <exception>
#include <iostream>
#include <stdexcept>
#include <vector>
#include "game/stdafx.h"
#include "common/packet_headers.h"
#include "common/tables.h"
#include "game/packet_structs.h"
#include "game/quest_packet.h"
#include "game/request_cooldown.h"
#include "game/SecureCipher.h"
#include "libthecore/fdwatch.h"
#include "libthecore/signal.h"
namespace
{
#pragma pack(push, 1)
struct WirePhasePacket
{
uint16_t header;
uint16_t length;
uint8_t phase;
};
struct WireKeyChallengePacket
{
uint16_t header;
uint16_t length;
uint8_t server_pk[SecureCipher::PK_SIZE];
uint8_t challenge[SecureCipher::CHALLENGE_SIZE];
uint32_t server_time;
};
struct WireKeyResponsePacket
{
uint16_t header;
uint16_t length;
uint8_t client_pk[SecureCipher::PK_SIZE];
uint8_t challenge_response[SecureCipher::HMAC_SIZE];
};
struct WireKeyCompletePacket
{
uint16_t header;
uint16_t length;
uint8_t encrypted_token[SecureCipher::SESSION_TOKEN_SIZE + SecureCipher::TAG_SIZE];
uint8_t nonce[SecureCipher::NONCE_SIZE];
};
#pragma pack(pop)
void Expect(bool condition, const char* message)
{
if (!condition)
throw std::runtime_error(message);
}
void WriteExact(int fd, const void* data, size_t length, const char* message)
{
const uint8_t* cursor = static_cast<const uint8_t*>(data);
size_t remaining = length;
while (remaining > 0)
{
const ssize_t written = write(fd, cursor, remaining);
Expect(written > 0, message);
cursor += written;
remaining -= static_cast<size_t>(written);
}
}
void ReadExact(int fd, void* data, size_t length, const char* message)
{
uint8_t* cursor = static_cast<uint8_t*>(data);
size_t remaining = length;
while (remaining > 0)
{
const ssize_t bytes_read = read(fd, cursor, remaining);
Expect(bytes_read > 0, message);
cursor += bytes_read;
remaining -= static_cast<size_t>(bytes_read);
}
}
void TestPacketLayouts()
{
Expect(sizeof(WirePhasePacket) == 5, "Unexpected phase wire size");
Expect(sizeof(WireKeyChallengePacket) == 72, "Unexpected key challenge wire size");
Expect(sizeof(WireKeyResponsePacket) == 68, "Unexpected key response wire size");
Expect(sizeof(WireKeyCompletePacket) == 76, "Unexpected key complete wire size");
}
void TestSecureCipherRoundTrip()
{
SecureCipher server;
SecureCipher client;
Expect(server.Initialize(), "Server SecureCipher init failed");
Expect(client.Initialize(), "Client SecureCipher init failed");
std::array<uint8_t, SecureCipher::PK_SIZE> server_pk {};
std::array<uint8_t, SecureCipher::PK_SIZE> client_pk {};
server.GetPublicKey(server_pk.data());
client.GetPublicKey(client_pk.data());
Expect(client.ComputeClientKeys(server_pk.data()), "Client session key derivation failed");
Expect(server.ComputeServerKeys(client_pk.data()), "Server session key derivation failed");
std::array<uint8_t, SecureCipher::CHALLENGE_SIZE> challenge {};
std::array<uint8_t, SecureCipher::HMAC_SIZE> response {};
server.GenerateChallenge(challenge.data());
client.ComputeChallengeResponse(challenge.data(), response.data());
Expect(server.VerifyChallengeResponse(challenge.data(), response.data()), "Challenge verification failed");
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> token {};
for (size_t i = 0; i < token.size(); ++i)
token[i] = static_cast<uint8_t>(i);
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE + SecureCipher::TAG_SIZE> ciphertext {};
std::array<uint8_t, SecureCipher::NONCE_SIZE> nonce {};
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> plaintext {};
Expect(server.EncryptToken(token.data(), token.size(), ciphertext.data(), nonce.data()), "Token encryption failed");
Expect(client.DecryptToken(ciphertext.data(), ciphertext.size(), nonce.data(), plaintext.data()), "Token decryption failed");
Expect(std::memcmp(token.data(), plaintext.data(), token.size()) == 0, "Token round-trip mismatch");
server.SetActivated(true);
client.SetActivated(true);
std::array<uint8_t, 96> payload {};
for (size_t i = 0; i < payload.size(); ++i)
payload[i] = static_cast<uint8_t>(0xA0 + (i % 31));
auto encrypted = payload;
server.EncryptInPlace(encrypted.data(), encrypted.size());
client.DecryptInPlace(encrypted.data(), encrypted.size());
Expect(encrypted == payload, "Server to client stream cipher round-trip failed");
auto reverse = payload;
client.EncryptInPlace(reverse.data(), reverse.size());
server.DecryptInPlace(reverse.data(), reverse.size());
Expect(reverse == payload, "Client to server stream cipher round-trip failed");
}
void TestSocketAuthWireFlow()
{
SecureCipher server;
SecureCipher client;
Expect(server.Initialize(), "Server auth cipher init failed");
Expect(client.Initialize(), "Client auth cipher init failed");
int sockets[2] = { -1, -1 };
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0, "socketpair for auth flow failed");
WirePhasePacket phase_packet {};
phase_packet.header = GC::PHASE;
phase_packet.length = sizeof(phase_packet);
phase_packet.phase = PHASE_HANDSHAKE;
WireKeyChallengePacket key_challenge {};
key_challenge.header = GC::KEY_CHALLENGE;
key_challenge.length = sizeof(key_challenge);
server.GetPublicKey(key_challenge.server_pk);
server.GenerateChallenge(key_challenge.challenge);
key_challenge.server_time = 0x12345678;
WriteExact(sockets[0], &phase_packet, sizeof(phase_packet), "Failed to write phase packet");
WriteExact(sockets[0], &key_challenge, sizeof(key_challenge), "Failed to write key challenge");
WirePhasePacket client_phase {};
WireKeyChallengePacket client_challenge {};
ReadExact(sockets[1], &client_phase, sizeof(client_phase), "Failed to read phase packet");
ReadExact(sockets[1], &client_challenge, sizeof(client_challenge), "Failed to read key challenge");
Expect(client_phase.header == GC::PHASE, "Unexpected phase header");
Expect(client_phase.length == sizeof(client_phase), "Unexpected phase packet length");
Expect(client_phase.phase == PHASE_HANDSHAKE, "Unexpected phase value");
Expect(client_challenge.header == GC::KEY_CHALLENGE, "Unexpected key challenge header");
Expect(client_challenge.length == sizeof(client_challenge), "Unexpected key challenge length");
Expect(std::memcmp(client_challenge.server_pk, key_challenge.server_pk, sizeof(key_challenge.server_pk)) == 0,
"Server public key changed on the wire");
Expect(std::memcmp(client_challenge.challenge, key_challenge.challenge, sizeof(key_challenge.challenge)) == 0,
"Challenge bytes changed on the wire");
Expect(client.ComputeClientKeys(client_challenge.server_pk), "Client auth key derivation failed");
WireKeyResponsePacket key_response {};
key_response.header = CG::KEY_RESPONSE;
key_response.length = sizeof(key_response);
client.GetPublicKey(key_response.client_pk);
client.ComputeChallengeResponse(client_challenge.challenge, key_response.challenge_response);
WriteExact(sockets[1], &key_response, sizeof(key_response), "Failed to write key response");
WireKeyResponsePacket server_response {};
ReadExact(sockets[0], &server_response, sizeof(server_response), "Failed to read key response");
Expect(server_response.header == CG::KEY_RESPONSE, "Unexpected key response header");
Expect(server_response.length == sizeof(server_response), "Unexpected key response length");
Expect(server.ComputeServerKeys(server_response.client_pk), "Server auth key derivation failed");
Expect(server.VerifyChallengeResponse(key_challenge.challenge, server_response.challenge_response),
"Server rejected challenge response");
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> session_token {};
for (size_t i = 0; i < session_token.size(); ++i)
session_token[i] = static_cast<uint8_t>(0x30 + i);
server.SetSessionToken(session_token.data());
WireKeyCompletePacket key_complete {};
key_complete.header = GC::KEY_COMPLETE;
key_complete.length = sizeof(key_complete);
Expect(server.EncryptToken(session_token.data(), session_token.size(), key_complete.encrypted_token, key_complete.nonce),
"Failed to encrypt key complete token");
WriteExact(sockets[0], &key_complete, sizeof(key_complete), "Failed to write key complete");
WireKeyCompletePacket client_complete {};
ReadExact(sockets[1], &client_complete, sizeof(client_complete), "Failed to read key complete");
Expect(client_complete.header == GC::KEY_COMPLETE, "Unexpected key complete header");
Expect(client_complete.length == sizeof(client_complete), "Unexpected key complete length");
std::array<uint8_t, SecureCipher::SESSION_TOKEN_SIZE> decrypted_token {};
Expect(client.DecryptToken(client_complete.encrypted_token, sizeof(client_complete.encrypted_token),
client_complete.nonce, decrypted_token.data()),
"Failed to decrypt key complete token");
Expect(decrypted_token == session_token, "Session token changed on the wire");
server.SetActivated(true);
client.SetSessionToken(decrypted_token.data());
client.SetActivated(true);
std::array<uint8_t, 32> payload {};
for (size_t i = 0; i < payload.size(); ++i)
payload[i] = static_cast<uint8_t>(0x41 + i);
auto encrypted_payload = payload;
server.EncryptInPlace(encrypted_payload.data(), encrypted_payload.size());
WriteExact(sockets[0], encrypted_payload.data(), encrypted_payload.size(), "Failed to write encrypted payload");
std::array<uint8_t, 32> received_payload {};
ReadExact(sockets[1], received_payload.data(), received_payload.size(), "Failed to read encrypted payload");
client.DecryptInPlace(received_payload.data(), received_payload.size());
Expect(received_payload == payload, "Encrypted payload round-trip mismatch");
close(sockets[0]);
close(sockets[1]);
}
void TestFdwatchReadAndOneshotWrite()
{
int sockets[2] = { -1, -1 };
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0, "socketpair failed");
LPFDWATCH fdw = fdwatch_new(64);
Expect(fdw != nullptr, "fdwatch_new failed");
int marker = 42;
fdwatch_add_fd(fdw, sockets[1], &marker, FDW_READ, false);
const uint8_t byte = 0x7F;
Expect(write(sockets[0], &byte, sizeof(byte)) == sizeof(byte), "socketpair write failed");
timeval timeout {};
timeout.tv_sec = 0;
timeout.tv_usec = 200000;
int num_events = fdwatch(fdw, &timeout);
Expect(num_events == 1, "Expected one read event");
Expect(fdwatch_get_client_data(fdw, 0) == &marker, "Unexpected client data");
Expect(fdwatch_check_event(fdw, sockets[1], 0) == FDW_READ, "Expected FDW_READ event");
uint8_t read_back = 0;
Expect(read(sockets[1], &read_back, sizeof(read_back)) == sizeof(read_back), "socketpair read failed");
Expect(read_back == byte, "Read payload mismatch");
fdwatch_add_fd(fdw, sockets[1], &marker, FDW_WRITE, true);
num_events = fdwatch(fdw, &timeout);
Expect(num_events >= 1, "Expected at least one write event");
Expect(fdwatch_check_event(fdw, sockets[1], 0) == FDW_WRITE, "Expected FDW_WRITE event");
timeout.tv_sec = 0;
timeout.tv_usec = 0;
num_events = fdwatch(fdw, &timeout);
Expect(num_events == 0, "FDW_WRITE oneshot was not cleared");
fdwatch_del_fd(fdw, sockets[1]);
fdwatch_delete(fdw);
close(sockets[0]);
close(sockets[1]);
}
void TestFdwatchBackendMetadata()
{
LPFDWATCH fdw = fdwatch_new(4096);
Expect(fdw != nullptr, "fdwatch_new for backend metadata failed");
#ifdef __linux__
Expect(fdwatch_get_backend(fdw) == FDWATCH_BACKEND_EPOLL, "Expected epoll backend");
Expect(std::strcmp(fdwatch_backend_name(fdwatch_get_backend(fdw)), "epoll") == 0, "Unexpected epoll backend name");
Expect(fdwatch_get_descriptor_limit(fdw) == 4096, "Unexpected epoll descriptor limit");
#elif defined(__USE_SELECT__)
Expect(fdwatch_get_backend(fdw) == FDWATCH_BACKEND_SELECT, "Expected select backend");
Expect(std::strcmp(fdwatch_backend_name(fdwatch_get_backend(fdw)), "select") == 0, "Unexpected select backend name");
Expect(fdwatch_get_descriptor_limit(fdw) == std::min(4096, static_cast<int>(FD_SETSIZE)), "Unexpected select descriptor limit");
#else
Expect(fdwatch_get_backend(fdw) == FDWATCH_BACKEND_KQUEUE, "Expected kqueue backend");
Expect(std::strcmp(fdwatch_backend_name(fdwatch_get_backend(fdw)), "kqueue") == 0, "Unexpected kqueue backend name");
Expect(fdwatch_get_descriptor_limit(fdw) == 4096, "Unexpected kqueue descriptor limit");
#endif
fdwatch_delete(fdw);
}
void TestCheckpointBackendMetadata()
{
#ifdef OS_WINDOWS
Expect(signal_checkpoint_backend() == CHECKPOINT_BACKEND_NONE, "Expected no checkpoint backend on Windows");
Expect(std::strcmp(signal_checkpoint_backend_name(signal_checkpoint_backend()), "none") == 0,
"Unexpected checkpoint backend name on Windows");
#else
Expect(signal_checkpoint_backend() == CHECKPOINT_BACKEND_WATCHDOG_THREAD, "Expected watchdog thread checkpoint backend");
Expect(std::strcmp(signal_checkpoint_backend_name(signal_checkpoint_backend()), "watchdog-thread") == 0,
"Unexpected checkpoint backend name");
#endif
}
void TestFdwatchSlotReuseAfterDelete()
{
int sockets_a[2] = { -1, -1 };
int sockets_b[2] = { -1, -1 };
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets_a) == 0, "socketpair A failed");
Expect(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets_b) == 0, "socketpair B failed");
LPFDWATCH fdw = fdwatch_new(64);
Expect(fdw != nullptr, "fdwatch_new for slot reuse failed");
int marker_a = 11;
int marker_b = 22;
fdwatch_add_fd(fdw, sockets_a[1], &marker_a, FDW_READ, false);
fdwatch_add_fd(fdw, sockets_b[1], &marker_b, FDW_READ, false);
fdwatch_del_fd(fdw, sockets_a[1]);
const uint8_t byte = 0x51;
Expect(write(sockets_b[0], &byte, sizeof(byte)) == sizeof(byte), "socketpair B write failed");
timeval timeout {};
timeout.tv_sec = 0;
timeout.tv_usec = 200000;
const int num_events = fdwatch(fdw, &timeout);
Expect(num_events == 1, "Expected one read event after slot reuse");
Expect(fdwatch_get_client_data(fdw, 0) == &marker_b, "Unexpected client data after slot reuse");
Expect(fdwatch_check_event(fdw, sockets_b[1], 0) == FDW_READ, "Expected FDW_READ after slot reuse");
uint8_t read_back = 0;
Expect(read(sockets_b[1], &read_back, sizeof(read_back)) == sizeof(read_back), "socketpair B read failed");
Expect(read_back == byte, "Read payload mismatch after slot reuse");
fdwatch_delete(fdw);
close(sockets_a[0]);
close(sockets_a[1]);
close(sockets_b[0]);
close(sockets_b[1]);
}
void TestQuestInfoPacketFraming()
{
quest::QuestInfoPacketData data {};
data.quest_index = 77;
data.send_flags = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6);
data.is_begin = true;
data.title = "Mall reward";
data.clock_name = "Soon";
data.clock_value = 15;
data.counter_name = "Kills";
data.counter_value = 2;
data.icon_file = "d:/icon/test.tga";
const auto quest_packet = quest::BuildQuestInfoPacket(data);
Expect(!quest_packet.empty(), "Quest info packet is empty");
Expect(quest_packet.size() == sizeof(packet_quest_info) + 1 + 31 + 17 + 4 + 17 + 4 + 25,
"Unexpected quest info packet size");
const auto* quest_header = reinterpret_cast<const packet_quest_info*>(quest_packet.data());
Expect(quest_header->header == GC::QUEST_INFO, "Unexpected quest info header");
Expect(quest_header->length == quest_packet.size(), "Quest info packet length does not match payload size");
Expect(quest_packet[sizeof(packet_quest_info)] == 1, "Quest begin flag payload mismatch");
TPacketGCItemGet item_get {};
item_get.header = GC::ITEM_GET;
item_get.length = sizeof(item_get);
item_get.dwItemVnum = 50187;
item_get.bCount = 1;
item_get.bArg = 0;
std::vector<uint8_t> stream = quest_packet;
const auto* item_bytes = reinterpret_cast<const uint8_t*>(&item_get);
stream.insert(stream.end(), item_bytes, item_bytes + sizeof(item_get));
const size_t next_frame_offset = quest_header->length;
Expect(stream.size() >= next_frame_offset + sizeof(item_get), "Combined stream truncated after quest packet");
const auto* next_frame = reinterpret_cast<const TPacketGCItemGet*>(stream.data() + next_frame_offset);
Expect(next_frame->header == GC::ITEM_GET, "Quest info packet left trailing bytes before next frame");
Expect(next_frame->length == sizeof(TPacketGCItemGet), "Item get packet length mismatch after quest packet");
}
void TestRequestCooldownGuard()
{
Expect(!HasRecentRequestCooldown(0, 5, 10), "Initial zero request pulse should not trigger cooldown");
Expect(HasRecentRequestCooldown(95, 100, 10), "Recent request pulse should still be on cooldown");
Expect(!HasRecentRequestCooldown(90, 100, 10), "Cooldown boundary should allow request");
}
}
int main()
{
try
{
TestPacketLayouts();
TestSecureCipherRoundTrip();
TestSocketAuthWireFlow();
TestFdwatchBackendMetadata();
TestCheckpointBackendMetadata();
TestFdwatchReadAndOneshotWrite();
TestFdwatchSlotReuseAfterDelete();
TestQuestInfoPacketFraming();
TestRequestCooldownGuard();
std::cout << "metin smoke tests passed\n";
return 0;
}
catch (const std::exception& e)
{
std::cerr << "metin smoke tests failed: " << e.what() << '\n';
return 1;
}
}