bare bones query manager
This is a bare bones query manager that will respond just enough queries to get the server running. The overall structure of the tables should be almost set but we'd need to handle all queries to get the server running properly. It uses the SQLite3 3.50.2 amalgamation for a database backend so we can completely avoid having to spin up yet another service for a separate database system, effectively turning the query manager into the database itself. This also means that other services such as the login server and website must go through it to access the data but it shouldn't be too difficult to replicate the querying mechanism from the server. This design choice was made with a single game server in mind. The networking protocol is NOT encrypted at all and won't accept remote connections. For a fully multi-world distributed infrastructure, a distributed database system like PostgreSQL and MySQL should be considered.
This commit is contained in:
commit
8dbd1d39e3
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.vscode
|
||||
bin
|
||||
build
|
||||
local
|
||||
*.log
|
||||
*.db
|
||||
44
Makefile
Normal file
44
Makefile
Normal file
@ -0,0 +1,44 @@
|
||||
SRCDIR = src
|
||||
BUILDDIR = build
|
||||
OUTPUTEXE = querymanager
|
||||
|
||||
CC = gcc
|
||||
CXX = g++
|
||||
CFLAGS = -m64 -fno-strict-aliasing -pedantic -Wall -Wextra -pthread
|
||||
CXXFLAGS = $(CFLAGS) --std=c++11
|
||||
LFLAGS = -Wl,-t
|
||||
|
||||
DEBUG ?= 0
|
||||
ifneq ($(DEBUG), 0)
|
||||
CFLAGS += -g -O0
|
||||
CXXFLAGS += -g -O0
|
||||
else
|
||||
CFLAGS += -O2
|
||||
CXXFLAGS += -O2
|
||||
endif
|
||||
|
||||
$(BUILDDIR)/$(OUTPUTEXE): $(BUILDDIR)/connections.obj $(BUILDDIR)/database.obj $(BUILDDIR)/querymanager.obj $(BUILDDIR)/sqlite3.obj
|
||||
@mkdir -p $(@D)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $^ $(LFLAGS)
|
||||
|
||||
$(BUILDDIR)/connections.obj: $(SRCDIR)/connections.cc $(SRCDIR)/querymanager.hh
|
||||
@mkdir -p $(@D)
|
||||
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
||||
|
||||
$(BUILDDIR)/database.obj: $(SRCDIR)/database.cc $(SRCDIR)/querymanager.hh
|
||||
@mkdir -p $(@D)
|
||||
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
||||
|
||||
$(BUILDDIR)/querymanager.obj: $(SRCDIR)/querymanager.cc $(SRCDIR)/querymanager.hh
|
||||
@mkdir -p $(@D)
|
||||
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
||||
|
||||
$(BUILDDIR)/sqlite3.obj: $(SRCDIR)/sqlite3.c $(SRCDIR)/sqlite3.h
|
||||
@mkdir -p $(@D)
|
||||
$(CC) -c $(CFLAGS) -o $@ $<
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILDDIR)
|
||||
|
||||
11
config.cfg
Normal file
11
config.cfg
Normal file
@ -0,0 +1,11 @@
|
||||
# Database
|
||||
DatabaseFile = "tibia.db"
|
||||
MaxCachedStatements = 100
|
||||
|
||||
# Connection
|
||||
Password = "a6glaf0c"
|
||||
Port = 7174
|
||||
MaxConnections = 50
|
||||
MaxConnectionIdleTime = 60s
|
||||
MaxConnectionPacketSize = 1M
|
||||
UpdateRate = 20
|
||||
10
sql/README.txt
Normal file
10
sql/README.txt
Normal file
@ -0,0 +1,10 @@
|
||||
The query manager will properly initialize and upgrade the database schema
|
||||
based on files in this folder. The initial schema should be in `schema.sql` and
|
||||
upgrades should be in `upgrade-N.sql` where N is a number starting from 1. The
|
||||
`upgrade-N.sql` file should take the database from version N to N + 1.
|
||||
The only restriction to these files is that they can't have transaction
|
||||
statements ("BEGIN", "ROLLBACK", "COMMIT") because the query manager will
|
||||
already bundle them into a transaction that also sets `user_version`, and
|
||||
and nested transactions aren't allowed.
|
||||
The current database version can be retrieved with the "PRAGMA user_version"
|
||||
query in the SQLite shell.
|
||||
177
sql/schema.sql
Normal file
177
sql/schema.sql
Normal file
@ -0,0 +1,177 @@
|
||||
CREATE TABLE IF NOT EXISTS Worlds (
|
||||
WorldID INTEGER NOT NULL,
|
||||
Name TEXT NOT NULL COLLATE NOCASE,
|
||||
Type INTEGER NOT NULL,
|
||||
RebootTime INTEGER NOT NULL,
|
||||
Address INTEGER NOT NULL,
|
||||
Port INTEGER NOT NULL,
|
||||
MaxPlayers INTEGER NOT NULL,
|
||||
PremiumPlayerBuffer INTEGER NOT NULL,
|
||||
MaxNewbies INTEGER NOT NULL,
|
||||
PremiumNewbieBuffer INTEGER NOT NULL,
|
||||
PRIMARY KEY (WorldID),
|
||||
UNIQUE (Name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Accounts (
|
||||
AccountID INTEGER NOT NULL,
|
||||
Auth BLOB NOT NULL,
|
||||
EMail TEXT NOT NULL COLLATE NOCASE,
|
||||
PremiumEnd INTEGER NOT NULL,
|
||||
AddPremiumDays INTEGER NOT NULL,
|
||||
PRIMARY KEY (AccountID),
|
||||
UNIQUE (EMail)
|
||||
);
|
||||
|
||||
-- Character Tables
|
||||
--==============================================================================
|
||||
CREATE TABLE IF NOT EXISTS Characters (
|
||||
WorldID INTEGER NOT NULL,
|
||||
CharacterID INTEGER NOT NULL,
|
||||
AccountID INTEGER NOT NULL,
|
||||
Name TEXT NOT NULL COLLATE NOCASE,
|
||||
Sex INTEGER NOT NULL,
|
||||
Guild TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
||||
Rank TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
||||
Title TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
||||
PRIMARY KEY (CharacterID),
|
||||
UNIQUE (Name)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS CharactersAccountIndex ON Characters(AccountID);
|
||||
CREATE INDEX IF NOT EXISTS CharactersGuildIndex ON Characters(Guild);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS CharacterBuddies (
|
||||
CharacterID INTEGER NOT NULL,
|
||||
BuddyID INTEGER NOT NULL,
|
||||
PRIMARY KEY (CharacterID, BuddyID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS CharacterInvitations (
|
||||
WorldID INTEGER NOT NULL,
|
||||
CharacterID INTEGER NOT NULL,
|
||||
PRIMARY KEY (WorldID, CharacterID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS CharacterRights (
|
||||
CharacterID INTEGER NOT NULL,
|
||||
Right TEXT NOT NULL COLLATE NOCASE,
|
||||
PRIMARY KEY(CharacterID, Right)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS OnlineCharacters (
|
||||
CharacterID INTEGER NOT NULL,
|
||||
AccountID INTEGER NOT NULL,
|
||||
IPAddress INTEGER NOT NULL,
|
||||
MultiClient INTEGER NOT NULL,
|
||||
PRIMARY KEY (CharacterID)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS OnlineCharactersAccountIndex ON OnlineCharacters(AccountID);
|
||||
|
||||
-- House Tables
|
||||
--==============================================================================
|
||||
CREATE TABLE IF NOT EXISTS Houses (
|
||||
WorldID INTEGER NOT NULL,
|
||||
HouseID INTEGER NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
Rent INTEGER NOT NULL,
|
||||
Description TEXT NOT NULL,
|
||||
Size INTEGER NOT NULL,
|
||||
PositionX INTEGER NOT NULL,
|
||||
PositionY INTEGER NOT NULL,
|
||||
PositionZ INTEGER NOT NULL,
|
||||
Town TEXT NOT NULL,
|
||||
GuildHouse INTEGER NOT NULL,
|
||||
PRIMARY KEY (WorldID, HouseID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS HouseOwners (
|
||||
WorldID INTEGER NOT NULL,
|
||||
HouseID INTEGER NOT NULL,
|
||||
OwnerID INTEGER NOT NULL,
|
||||
PaidUntil INTEGER NOT NULL,
|
||||
PRIMARY KEY (WorldID, HouseID)
|
||||
);
|
||||
|
||||
-- NOTE(fusion): An auction would have a non null `FinishTime` but it doesn't make
|
||||
-- sense to finish an auction just to restart it afterwards so it should only be
|
||||
-- set after the first bid, along with `BidderID` and `BidAmount`.
|
||||
CREATE TABLE IF NOT EXISTS HouseAuctions (
|
||||
WorldID INTEGER NOT NULL,
|
||||
HouseID INTEGER NOT NULL,
|
||||
BidderID INTEGER,
|
||||
BidAmount INTEGER,
|
||||
FinishTime INTEGER,
|
||||
PRIMARY KEY (WorldID, HouseID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS HouseTransfers (
|
||||
WorldID INTEGER NOT NULL,
|
||||
HouseID INTEGER NOT NULL,
|
||||
NewOwnerID INTEGER NOT NULL,
|
||||
Price INTEGER NOT NULL,
|
||||
PRIMARY KEY (WorldID, HouseID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS HouseAssignments (
|
||||
WorldID INTEGER NOT NULL,
|
||||
HouseID INTEGER NOT NULL,
|
||||
OwnerID INTEGER NOT NULL,
|
||||
Price INTEGER NOT NULL,
|
||||
Timestamp INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS HouseAssignmentsHouseIndex ON HouseAssignments(WorldID, HouseID);
|
||||
CREATE INDEX IF NOT EXISTS HouseAssignmentsOwnerIndex ON HouseAssignments(OwnerID);
|
||||
|
||||
-- Banishment Tables
|
||||
--==============================================================================
|
||||
CREATE TABLE IF NOT EXISTS Banishments (
|
||||
BanishmentID INTEGER NOT NULL,
|
||||
AccountID INTEGER NOT NULL,
|
||||
IPAddress INTEGER NOT NULL,
|
||||
GamemasterID INTEGER NOT NULL,
|
||||
Reason TEXT NOT NULL,
|
||||
Comment TEXT NOT NULL,
|
||||
FinalWarning INTEGER NOT NULL,
|
||||
Until INTEGER NOT NULL,
|
||||
PRIMARY KEY (BanishmentID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS IPBanishments (
|
||||
IPAddress INTEGER NOT NULL,
|
||||
CharacterID INTEGER NOT NULL,
|
||||
GamemasterID INTEGER NOT NULL,
|
||||
Reason TEXT NOT NULL,
|
||||
Comment TEXT NOT NULL,
|
||||
PRIMARY KEY (IPAddress)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Namelocks (
|
||||
CharacterID INTEGER NOT NULL,
|
||||
IPAddress INTEGER NOT NULL,
|
||||
GamemasterID INTEGER NOT NULL,
|
||||
Reason TEXT NOT NULL,
|
||||
Comment TEXT NOT NULL,
|
||||
PRIMARY KEY (CharacterID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Notations (
|
||||
CharacterID INTEGER NOT NULL,
|
||||
IPAddress INTEGER NOT NULL,
|
||||
GamemasterID INTEGER NOT NULL,
|
||||
Reason TEXT NOT NULL,
|
||||
Comment TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS NotationsCharacterIndex ON Notations(CharacterID);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Statements (
|
||||
Timestamp INTEGER NOT NULL,
|
||||
StatementID INTEGER NOT NULL,
|
||||
CharacterID INTEGER NOT NULL,
|
||||
Statement TEXT NOT NULL,
|
||||
Context TEXT NOT NULL,
|
||||
BanishmentID INTEGER NOT NULL,
|
||||
ReporterID INTEGER NOT NULL,
|
||||
Reason TEXT NOT NULL,
|
||||
Comment TEXT NOT NULL,
|
||||
PRIMARY KEY (Timestamp, StatementID)
|
||||
);
|
||||
799
src/connections.cc
Normal file
799
src/connections.cc
Normal file
@ -0,0 +1,799 @@
|
||||
#include "querymanager.hh"
|
||||
|
||||
// TODO(fusion): Eventually support Windows? It's not that difficult to wrap the
|
||||
// few OS specific calls but it can be annoying plus the socket type is slightly
|
||||
// different.
|
||||
#if OS_LINUX
|
||||
# include <errno.h>
|
||||
# include <fcntl.h>
|
||||
# include <netinet/in.h>
|
||||
# include <poll.h>
|
||||
# include <sys/socket.h>
|
||||
# include <unistd.h>
|
||||
# include <time.h>
|
||||
#else
|
||||
# error "Operating system not currently supported."
|
||||
#endif
|
||||
|
||||
static int g_Listener = -1;
|
||||
static TConnection *g_Connections;
|
||||
|
||||
// Connection Handling
|
||||
//==============================================================================
|
||||
int ListenerBind(uint16 Port){
|
||||
int Socket = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(Socket == -1){
|
||||
LOG_ERR("Failed to create listener socket: (%d) %s", errno, strerrordesc_np(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ReuseAddr = 1;
|
||||
if(setsockopt(Socket, SOL_SOCKET, SO_REUSEADDR, &ReuseAddr, sizeof(ReuseAddr)) == -1){
|
||||
LOG_ERR("Failed to set SO_REUSADDR: (%d) %s", errno, strerrordesc_np(errno));
|
||||
close(Socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int Flags = fcntl(Socket, F_GETFL);
|
||||
if(Flags == -1){
|
||||
LOG_ERR("Failed to get socket flags: (%d) %s", errno, strerrordesc_np(errno));
|
||||
close(Socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(fcntl(Socket, F_SETFL, Flags | O_NONBLOCK) == -1){
|
||||
LOG_ERR("Failed to set socket flags: (%d) %s", errno, strerrordesc_np(errno));
|
||||
close(Socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// IMPORTANT(fusion): Binding the socket to the LOOPBACK address should allow
|
||||
// only local connections to be accepted. This is VERY important as the protocol
|
||||
// IS NOT encrypted.
|
||||
sockaddr_in Addr = {};
|
||||
Addr.sin_family = AF_INET;
|
||||
Addr.sin_port = htons(Port);
|
||||
Addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
if(bind(Socket, (sockaddr*)&Addr, sizeof(Addr)) == -1){
|
||||
LOG_ERR("Failed to bind socket to port %d: (%d) %s", Port, errno, strerrordesc_np(errno));
|
||||
close(Socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(listen(Socket, 128) == -1){
|
||||
LOG_ERR("Failed to listen to port %d: (%d) %s", Port, errno, strerrordesc_np(errno));
|
||||
close(Socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return Socket;
|
||||
}
|
||||
|
||||
int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort){
|
||||
while(true){
|
||||
sockaddr_in SocketAddr = {};
|
||||
socklen_t SocketAddrLen = sizeof(SocketAddr);
|
||||
int Socket = accept(Listener, (sockaddr*)&SocketAddr, &SocketAddrLen);
|
||||
if(Socket == -1){
|
||||
if(errno != EAGAIN){
|
||||
LOG_ERR("Failed to accept connection: (%d) %s", errno, strerrordesc_np(errno));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// IMPORTANT(fusion): It should be impossible to spoof the loopback
|
||||
// address so this comparison should be safe. We're also binding the
|
||||
// listening socket to the loopback address which should prevent any
|
||||
// other address to show up here.
|
||||
uint32 Addr = ntohl(SocketAddr.sin_addr.s_addr);
|
||||
uint16 Port = ntohs(SocketAddr.sin_port);
|
||||
if(Addr != INADDR_LOOPBACK){
|
||||
LOG_ERR("Rejecting remote connection from %08X.", Addr);
|
||||
close(Socket);
|
||||
continue;
|
||||
}
|
||||
|
||||
int Flags = fcntl(Socket, F_GETFL);
|
||||
if(Flags == -1){
|
||||
LOG_ERR("Failed to get socket flags: (%d) %s", errno, strerrordesc_np(errno));
|
||||
close(Socket);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(fcntl(Socket, F_SETFL, Flags | O_NONBLOCK) == -1){
|
||||
LOG_ERR("Failed to set socket flags: (%d) %s", errno, strerrordesc_np(errno));
|
||||
close(Socket);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(OutAddr){
|
||||
*OutAddr = Addr;
|
||||
}
|
||||
|
||||
if(OutPort){
|
||||
*OutPort = Port;
|
||||
}
|
||||
|
||||
return Socket;
|
||||
}
|
||||
}
|
||||
|
||||
void CloseConnection(TConnection *Connection){
|
||||
if(Connection->Socket != -1){
|
||||
close(Connection->Socket);
|
||||
Connection->Socket = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void EnsureConnectionBuffer(TConnection *Connection){
|
||||
if(Connection->Buffer == NULL){
|
||||
Connection->Buffer = (uint8*)malloc(g_MaxConnectionPacketSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteConnectionBuffer(TConnection *Connection){
|
||||
if(Connection->Buffer != NULL){
|
||||
free(Connection->Buffer);
|
||||
Connection->Buffer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port){
|
||||
int ConnectionIndex = -1;
|
||||
for(int i = 0; i < g_MaxConnections; i += 1){
|
||||
if(g_Connections[i].State == CONNECTION_FREE){
|
||||
ConnectionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TConnection *Connection = NULL;
|
||||
if(ConnectionIndex != -1){
|
||||
Connection = &g_Connections[ConnectionIndex];
|
||||
Connection->State = CONNECTION_READING;
|
||||
Connection->Socket = Socket;
|
||||
Connection->LastActive = g_MonotonicTimeMS;
|
||||
snprintf(Connection->RemoteAddress,
|
||||
sizeof(Connection->RemoteAddress),
|
||||
"%d.%d.%d.%d:%d",
|
||||
((int)(Addr >> 24) & 0xFF),
|
||||
((int)(Addr >> 16) & 0xFF),
|
||||
((int)(Addr >> 8) & 0xFF),
|
||||
((int)(Addr >> 0) & 0xFF),
|
||||
(int)Port);
|
||||
|
||||
LOG("Connection %s assigned to slot %d",
|
||||
Connection->RemoteAddress, ConnectionIndex);
|
||||
}
|
||||
return Connection;
|
||||
}
|
||||
|
||||
void ReleaseConnection(TConnection *Connection){
|
||||
if(Connection->State != CONNECTION_FREE){
|
||||
LOG("Connection %s released", Connection->RemoteAddress);
|
||||
CloseConnection(Connection);
|
||||
DeleteConnectionBuffer(Connection);
|
||||
memset(Connection, 0, sizeof(TConnection));
|
||||
Connection->State = CONNECTION_FREE;
|
||||
Connection->Socket = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void CheckConnectionInput(TConnection *Connection, int Events){
|
||||
if((Events & POLLIN) == 0 || Connection->Socket == -1){
|
||||
return;
|
||||
}
|
||||
|
||||
if(Connection->State != CONNECTION_READING){
|
||||
LOG_ERR("Connection %s (State: %d) sending out-of-order data",
|
||||
Connection->RemoteAddress, Connection->State);
|
||||
CloseConnection(Connection);
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureConnectionBuffer(Connection);
|
||||
while(true){
|
||||
int ReadSize = Connection->RWSize;
|
||||
if(ReadSize == 0){
|
||||
if(Connection->RWPosition < 2){
|
||||
ReadSize = 2 - Connection->RWPosition;
|
||||
}else{
|
||||
ReadSize = 6 - Connection->RWPosition;
|
||||
}
|
||||
ASSERT(ReadSize > 0);
|
||||
}
|
||||
|
||||
int BytesRead = read(Connection->Socket,
|
||||
(Connection->Buffer + Connection->RWPosition),
|
||||
(ReadSize - Connection->RWPosition));
|
||||
if(BytesRead == -1){
|
||||
if(errno != EAGAIN){
|
||||
// NOTE(fusion): Connection error.
|
||||
CloseConnection(Connection);
|
||||
}
|
||||
break;
|
||||
}else if(BytesRead == 0){
|
||||
// NOTE(fusion): Graceful close.
|
||||
CloseConnection(Connection);
|
||||
break;
|
||||
}
|
||||
|
||||
Connection->RWPosition += BytesRead;
|
||||
if(Connection->RWPosition >= ReadSize){
|
||||
if(Connection->RWSize != 0){
|
||||
Connection->State = CONNECTION_PROCESSING;
|
||||
Connection->LastActive = g_MonotonicTimeMS;
|
||||
break;
|
||||
}else if(Connection->RWPosition == 2){
|
||||
int PayloadSize = BufferRead16LE(Connection->Buffer);
|
||||
if(PayloadSize <= 0 || PayloadSize > g_MaxConnectionPacketSize){
|
||||
CloseConnection(Connection);
|
||||
break;
|
||||
}
|
||||
|
||||
if(PayloadSize != 0xFFFF){
|
||||
Connection->RWSize = PayloadSize;
|
||||
Connection->RWPosition = 0;
|
||||
}
|
||||
}else if(Connection->RWPosition == 6){
|
||||
int PayloadSize = (int)BufferRead32LE(Connection->Buffer + 2);
|
||||
if(PayloadSize <= 0 || PayloadSize > g_MaxConnectionPacketSize){
|
||||
CloseConnection(Connection);
|
||||
break;
|
||||
}
|
||||
|
||||
Connection->RWSize = PayloadSize;
|
||||
Connection->RWPosition = 0;
|
||||
}else{
|
||||
PANIC("Invalid input state (State: %d, RWSize: %d, RWPosition: %d)",
|
||||
Connection->State, Connection->RWSize, Connection->RWPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(Connection->State == CONNECTION_PROCESSING){
|
||||
ProcessConnectionQuery(Connection);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckConnectionOutput(TConnection *Connection, int Events){
|
||||
if((Events & POLLOUT) == 0 || Connection->Socket == -1){
|
||||
return;
|
||||
}
|
||||
|
||||
if(Connection->State != CONNECTION_WRITING){
|
||||
return;
|
||||
}
|
||||
|
||||
while(true){
|
||||
int BytesWritten = write(Connection->Socket,
|
||||
(Connection->Buffer + Connection->RWPosition),
|
||||
(Connection->RWSize - Connection->RWPosition));
|
||||
if(BytesWritten == -1){
|
||||
if(errno != EAGAIN){
|
||||
CloseConnection(Connection);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Connection->RWPosition += BytesWritten;
|
||||
if(Connection->RWPosition >= Connection->RWSize){
|
||||
Connection->State = CONNECTION_READING;
|
||||
Connection->RWSize = 0;
|
||||
Connection->RWPosition = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckConnection(TConnection *Connection, int Events){
|
||||
ASSERT((Events & POLLNVAL) == 0);
|
||||
|
||||
if((Events & (POLLERR | POLLHUP)) != 0){
|
||||
CloseConnection(Connection);
|
||||
}
|
||||
|
||||
if(g_MaxConnectionIdleTime > 0){
|
||||
int IdleTime = (g_MonotonicTimeMS - Connection->LastActive);
|
||||
if(IdleTime >= g_MaxConnectionIdleTime){
|
||||
LOG_WARN("Dropping connection %s due to inactivity",
|
||||
Connection->RemoteAddress);
|
||||
CloseConnection(Connection);
|
||||
}
|
||||
}
|
||||
|
||||
if(Connection->Socket == -1){
|
||||
ReleaseConnection(Connection);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessConnections(void){
|
||||
// NOTE(fusion): Accept new connections.
|
||||
while(true){
|
||||
uint32 Addr;
|
||||
uint16 Port;
|
||||
int Socket = ListenerAccept(g_Listener, &Addr, &Port);
|
||||
if(Socket == -1){
|
||||
break;
|
||||
}
|
||||
|
||||
if(AssignConnection(Socket, Addr, Port) == NULL){
|
||||
LOG_ERR("Rejecting connection from %08X due to max number of"
|
||||
" connections being reached (%d)", Addr, g_MaxConnections);
|
||||
close(Socket);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(fusion): Gather active connections.
|
||||
int NumConnections = 0;
|
||||
int *ConnectionIndices = (int*)alloca(g_MaxConnections * sizeof(int));
|
||||
pollfd *ConnectionFds = (pollfd*)alloca(g_MaxConnections * sizeof(pollfd));
|
||||
for(int i = 0; i < g_MaxConnections; i += 1){
|
||||
if(g_Connections[i].State == CONNECTION_FREE || g_Connections[i].Socket == -1){
|
||||
continue;
|
||||
}
|
||||
|
||||
ConnectionIndices[NumConnections] = i;
|
||||
ConnectionFds[NumConnections].fd = g_Connections[i].Socket;
|
||||
ConnectionFds[NumConnections].events = POLLIN | POLLOUT;
|
||||
ConnectionFds[NumConnections].revents = 0;
|
||||
NumConnections += 1;
|
||||
}
|
||||
|
||||
if(NumConnections <= 0){
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Poll connections.
|
||||
int NumEvents = poll(ConnectionFds, NumConnections, 0);
|
||||
if(NumEvents == -1){
|
||||
LOG_ERR("Failed to poll connections: (%d) %s", errno, strerrordesc_np(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Process connections.
|
||||
for(int i = 0; i < NumConnections; i += 1){
|
||||
TConnection *Connection = &g_Connections[ConnectionIndices[i]];
|
||||
int Events = (int)ConnectionFds[i].revents;
|
||||
CheckConnectionInput(Connection, Events);
|
||||
CheckConnectionOutput(Connection, Events);
|
||||
CheckConnection(Connection, Events);
|
||||
}
|
||||
}
|
||||
|
||||
bool InitConnections(void){
|
||||
ASSERT(g_Listener == -1);
|
||||
ASSERT(g_Connections == NULL);
|
||||
|
||||
LOG("Listening port: %d", g_Port);
|
||||
LOG("Max connections: %d", g_MaxConnections);
|
||||
LOG("Max connection idle time: %d ms", g_MaxConnectionIdleTime);
|
||||
LOG("Max connection packet size: %d", g_MaxConnectionPacketSize);
|
||||
|
||||
g_Listener = ListenerBind((uint16)g_Port);
|
||||
if(g_Listener == -1){
|
||||
LOG_ERR("Failed to bind listener");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_Connections = (TConnection*)calloc(
|
||||
g_MaxConnections, sizeof(TConnection));
|
||||
for(int i = 0; i < g_MaxConnections; i += 1){
|
||||
g_Connections[i].State = CONNECTION_FREE;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ExitConnections(void){
|
||||
if(g_Listener != -1){
|
||||
close(g_Listener);
|
||||
g_Listener = -1;
|
||||
}
|
||||
|
||||
if(g_Connections != NULL){
|
||||
for(int i = 0; i < g_MaxConnections; i += 1){
|
||||
ReleaseConnection(&g_Connections[i]);
|
||||
}
|
||||
|
||||
free(g_Connections);
|
||||
}
|
||||
}
|
||||
|
||||
// Connection Queries
|
||||
//==============================================================================
|
||||
TWriteBuffer PrepareResponse(TConnection *Connection, int Status){
|
||||
if(Connection->State != CONNECTION_PROCESSING){
|
||||
LOG_ERR("Connection %s is not processing query (State: %d)",
|
||||
Connection->RemoteAddress, Connection->State);
|
||||
CloseConnection(Connection);
|
||||
return TWriteBuffer(NULL, 0);
|
||||
}
|
||||
|
||||
TWriteBuffer WriteBuffer(Connection->Buffer, g_MaxConnectionPacketSize);
|
||||
WriteBuffer.Write16(0);
|
||||
WriteBuffer.Write8((uint8)Status);
|
||||
return WriteBuffer;
|
||||
}
|
||||
|
||||
void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer){
|
||||
if(Connection->State != CONNECTION_PROCESSING){
|
||||
LOG_ERR("Connection %s is not processing query (State: %d)",
|
||||
Connection->RemoteAddress, Connection->State);
|
||||
CloseConnection(Connection);
|
||||
return;
|
||||
}
|
||||
|
||||
ASSERT(WriteBuffer != NULL
|
||||
&& WriteBuffer->Buffer == Connection->Buffer
|
||||
&& WriteBuffer->Size == g_MaxConnectionPacketSize
|
||||
&& WriteBuffer->Position > 2);
|
||||
|
||||
int PayloadSize = WriteBuffer->Position - 2;
|
||||
if(PayloadSize < 0xFFFF){
|
||||
WriteBuffer->Rewrite16(0, (uint16)PayloadSize);
|
||||
}else{
|
||||
WriteBuffer->Rewrite16(0, 0xFFFF);
|
||||
WriteBuffer->Insert32(2, (uint32)PayloadSize);
|
||||
}
|
||||
|
||||
if(!WriteBuffer->Overflowed()){
|
||||
Connection->State = CONNECTION_WRITING;
|
||||
Connection->RWSize = WriteBuffer->Position;
|
||||
Connection->RWPosition = 0;
|
||||
}else{
|
||||
LOG_ERR("Write buffer overflowed when writing response to %s",
|
||||
Connection->RemoteAddress);
|
||||
CloseConnection(Connection);
|
||||
}
|
||||
}
|
||||
|
||||
void SendQueryStatusOk(TConnection *Connection){
|
||||
TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK);
|
||||
SendResponse(Connection, &WriteBuffer);
|
||||
}
|
||||
|
||||
void SendQueryStatusError(TConnection *Connection, int ErrorCode){
|
||||
TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_ERROR);
|
||||
WriteBuffer.Write8((uint8)ErrorCode);
|
||||
SendResponse(Connection, &WriteBuffer);
|
||||
}
|
||||
|
||||
void SendQueryStatusFailed(TConnection *Connection){
|
||||
TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_FAILED);
|
||||
SendResponse(Connection, &WriteBuffer);
|
||||
}
|
||||
|
||||
void ProcessLoginQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
char Password[30];
|
||||
char ApplicationData[30];
|
||||
int ApplicationType = Buffer->Read8();
|
||||
Buffer->ReadString(Password, sizeof(Password));
|
||||
if(ApplicationType == APPLICATION_TYPE_GAME){
|
||||
Buffer->ReadString(ApplicationData, sizeof(ApplicationData));
|
||||
}
|
||||
|
||||
if(!StringEq(g_Password, Password)){
|
||||
LOG_WARN("Invalid login attempt from %s", Connection->RemoteAddress);
|
||||
SendQueryStatusFailed(Connection);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG("Connection %s AUTHORIZED", Connection->RemoteAddress);
|
||||
Connection->Authorized = true;
|
||||
Connection->ApplicationType = ApplicationType;
|
||||
StringCopy(Connection->ApplicationData,
|
||||
sizeof(Connection->ApplicationData),
|
||||
ApplicationData);
|
||||
SendQueryStatusOk(Connection);
|
||||
}
|
||||
|
||||
void ProcessCheckAccountPasswordQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessLoginAdminQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessLoginGameQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessLogoutGameQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessSetNamelockQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessBanishAccountQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessSetNotationQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessReportStatementQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessBanishIpAddressQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessLogCharacterDeathQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessAddBuddyQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessRemoveBuddyQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessDecrementIsOnlineQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessFinishAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessTransferHousesQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessEvictFreeAccountsQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessEvictDeletedCharactersQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessEvictExGuildleadersQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessInsertHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessUpdateHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessDeleteHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessGetHouseOwnersQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
if(Connection->ApplicationType != APPLICATION_TYPE_GAME){
|
||||
SendQueryStatusFailed(Connection);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicArray<THouseOwner> HouseOwners;
|
||||
const char *WorldName = Connection->ApplicationData;
|
||||
if(!LoadHouseOwners(WorldName, &HouseOwners)){
|
||||
SendQueryStatusFailed(Connection);
|
||||
return;
|
||||
}
|
||||
|
||||
TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK);
|
||||
int NumHouseOwners = std::min<int>(HouseOwners.Length(), UINT16_MAX);
|
||||
WriteBuffer.Write16((uint16)NumHouseOwners);
|
||||
for(int i = 0; i < NumHouseOwners; i += 1){
|
||||
WriteBuffer.Write16((uint16)HouseOwners[i].HouseID);
|
||||
WriteBuffer.Write32((uint32)HouseOwners[i].OwnerID);
|
||||
WriteBuffer.WriteString(HouseOwners[i].OwnerName);
|
||||
WriteBuffer.Write32((uint32)HouseOwners[i].PaidUntil);
|
||||
}
|
||||
SendResponse(Connection, &WriteBuffer);
|
||||
}
|
||||
|
||||
void ProcessGetAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessStartAuctionQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessInsertHousesQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessClearIsOnlineQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessCreatePlayerlistQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessLogKilledCreaturesQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessLoadPlayersQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessExcludeFromAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessCancelHouseTransferQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessLoadWorldConfigQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
if(Connection->ApplicationType != APPLICATION_TYPE_GAME){
|
||||
SendQueryStatusFailed(Connection);
|
||||
return;
|
||||
}
|
||||
|
||||
TWorldConfig WorldConfig = {};
|
||||
const char *WorldName = Connection->ApplicationData;
|
||||
if(!LoadWorldConfig(WorldName, &WorldConfig)){
|
||||
SendQueryStatusFailed(Connection);
|
||||
return;
|
||||
}
|
||||
|
||||
TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK);
|
||||
WriteBuffer.Write8((uint8)WorldConfig.Type);
|
||||
WriteBuffer.Write8((uint8)WorldConfig.RebootTime);
|
||||
WriteBuffer.Write32BE((uint32)WorldConfig.Address);
|
||||
WriteBuffer.Write16((uint16)WorldConfig.Port);
|
||||
WriteBuffer.Write16((uint16)WorldConfig.MaxPlayers);
|
||||
WriteBuffer.Write16((uint16)WorldConfig.PremiumPlayerBuffer);
|
||||
WriteBuffer.Write16((uint16)WorldConfig.MaxNewbies);
|
||||
WriteBuffer.Write16((uint16)WorldConfig.PremiumNewbieBuffer);
|
||||
SendResponse(Connection, &WriteBuffer);
|
||||
}
|
||||
|
||||
void ProcessGetKeptCharactersQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessGetDeletedCharactersQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessDeleteOldCharacterQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessGetHiddenCharactersQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessCreateHighscoresQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessCreateCensusQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessCreateKillStatisticsQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessGetPlayersOnlineQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessGetWorldsQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessGetServerLoadQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessInsertPaymentDataOldQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessAddPaymentOldQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessCancelPaymentOldQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessInsertPaymentDataNewQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessAddPaymentNewQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessCancelPaymentNewQuery(TConnection *Connection, TReadBuffer *Buffer){
|
||||
SendQueryStatusFailed(Connection);
|
||||
}
|
||||
|
||||
void ProcessConnectionQuery(TConnection *Connection){
|
||||
// TODO(fusion): Ideally we'd create a new query and dispatch it to the
|
||||
// database thread for processing. Realistically, it wouldn't make a big
|
||||
// difference since we're already handling connections asynchronously and
|
||||
// the only blocking system calls are done by SQLite when interacting with
|
||||
// the disk.
|
||||
|
||||
TReadBuffer Buffer(Connection->Buffer, Connection->RWSize);
|
||||
uint8 Query = Buffer.Read8();
|
||||
if(!Connection->Authorized){
|
||||
if(Query == QUERY_LOGIN){
|
||||
ProcessLoginQuery(Connection, &Buffer);
|
||||
}else{
|
||||
LOG_ERR("Expected login query");
|
||||
CloseConnection(Connection);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch(Query){
|
||||
case QUERY_CHECK_ACCOUNT_PASSWORD: ProcessCheckAccountPasswordQuery(Connection, &Buffer); break;
|
||||
case QUERY_LOGIN_ADMIN: ProcessLoginAdminQuery(Connection, &Buffer); break;
|
||||
case QUERY_LOGIN_GAME: ProcessLoginGameQuery(Connection, &Buffer); break;
|
||||
case QUERY_LOGOUT_GAME: ProcessLogoutGameQuery(Connection, &Buffer); break;
|
||||
case QUERY_SET_NAMELOCK: ProcessSetNamelockQuery(Connection, &Buffer); break;
|
||||
case QUERY_BANISH_ACCOUNT: ProcessBanishAccountQuery(Connection, &Buffer); break;
|
||||
case QUERY_SET_NOTATION: ProcessSetNotationQuery(Connection, &Buffer); break;
|
||||
case QUERY_REPORT_STATEMENT: ProcessReportStatementQuery(Connection, &Buffer); break;
|
||||
case QUERY_BANISH_IP_ADDRESS: ProcessBanishIpAddressQuery(Connection, &Buffer); break;
|
||||
case QUERY_LOG_CHARACTER_DEATH: ProcessLogCharacterDeathQuery(Connection, &Buffer); break;
|
||||
case QUERY_ADD_BUDDY: ProcessAddBuddyQuery(Connection, &Buffer); break;
|
||||
case QUERY_REMOVE_BUDDY: ProcessRemoveBuddyQuery(Connection, &Buffer); break;
|
||||
case QUERY_DECREMENT_IS_ONLINE: ProcessDecrementIsOnlineQuery(Connection, &Buffer); break;
|
||||
case QUERY_FINISH_AUCTIONS: ProcessFinishAuctionsQuery(Connection, &Buffer); break;
|
||||
case QUERY_TRANSFER_HOUSES: ProcessTransferHousesQuery(Connection, &Buffer); break;
|
||||
case QUERY_EVICT_FREE_ACCOUNTS: ProcessEvictFreeAccountsQuery(Connection, &Buffer); break;
|
||||
case QUERY_EVICT_DELETED_CHARACTERS: ProcessEvictDeletedCharactersQuery(Connection, &Buffer); break;
|
||||
case QUERY_EVICT_EX_GUILDLEADERS: ProcessEvictExGuildleadersQuery(Connection, &Buffer); break;
|
||||
case QUERY_INSERT_HOUSE_OWNER: ProcessInsertHouseOwnerQuery(Connection, &Buffer); break;
|
||||
case QUERY_UPDATE_HOUSE_OWNER: ProcessUpdateHouseOwnerQuery(Connection, &Buffer); break;
|
||||
case QUERY_DELETE_HOUSE_OWNER: ProcessDeleteHouseOwnerQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_HOUSE_OWNERS: ProcessGetHouseOwnersQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_AUCTIONS: ProcessGetAuctionsQuery(Connection, &Buffer); break;
|
||||
case QUERY_START_AUCTION: ProcessStartAuctionQuery(Connection, &Buffer); break;
|
||||
case QUERY_INSERT_HOUSES: ProcessInsertHousesQuery(Connection, &Buffer); break;
|
||||
case QUERY_CLEAR_IS_ONLINE: ProcessClearIsOnlineQuery(Connection, &Buffer); break;
|
||||
case QUERY_CREATE_PLAYERLIST: ProcessCreatePlayerlistQuery(Connection, &Buffer); break;
|
||||
case QUERY_LOG_KILLED_CREATURES: ProcessLogKilledCreaturesQuery(Connection, &Buffer); break;
|
||||
case QUERY_LOAD_PLAYERS: ProcessLoadPlayersQuery(Connection, &Buffer); break;
|
||||
case QUERY_EXCLUDE_FROM_AUCTIONS: ProcessExcludeFromAuctionsQuery(Connection, &Buffer); break;
|
||||
case QUERY_CANCEL_HOUSE_TRANSFER: ProcessCancelHouseTransferQuery(Connection, &Buffer); break;
|
||||
case QUERY_LOAD_WORLD_CONFIG: ProcessLoadWorldConfigQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_KEPT_CHARACTERS: ProcessGetKeptCharactersQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_DELETED_CHARACTERS: ProcessGetDeletedCharactersQuery(Connection, &Buffer); break;
|
||||
case QUERY_DELETE_OLD_CHARACTER: ProcessDeleteOldCharacterQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_HIDDEN_CHARACTERS: ProcessGetHiddenCharactersQuery(Connection, &Buffer); break;
|
||||
case QUERY_CREATE_HIGHSCORES: ProcessCreateHighscoresQuery(Connection, &Buffer); break;
|
||||
case QUERY_CREATE_CENSUS: ProcessCreateCensusQuery(Connection, &Buffer); break;
|
||||
case QUERY_CREATE_KILL_STATISTICS: ProcessCreateKillStatisticsQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_PLAYERS_ONLINE: ProcessGetPlayersOnlineQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_WORLDS: ProcessGetWorldsQuery(Connection, &Buffer); break;
|
||||
case QUERY_GET_SERVER_LOAD: ProcessGetServerLoadQuery(Connection, &Buffer); break;
|
||||
case QUERY_INSERT_PAYMENT_DATA_OLD: ProcessInsertPaymentDataOldQuery(Connection, &Buffer); break;
|
||||
case QUERY_ADD_PAYMENT_OLD: ProcessAddPaymentOldQuery(Connection, &Buffer); break;
|
||||
case QUERY_CANCEL_PAYMENT_OLD: ProcessCancelPaymentOldQuery(Connection, &Buffer); break;
|
||||
case QUERY_INSERT_PAYMENT_DATA_NEW: ProcessInsertPaymentDataNewQuery(Connection, &Buffer); break;
|
||||
case QUERY_ADD_PAYMENT_NEW: ProcessAddPaymentNewQuery(Connection, &Buffer); break;
|
||||
case QUERY_CANCEL_PAYMENT_NEW: ProcessCancelPaymentNewQuery(Connection, &Buffer); break;
|
||||
default:{
|
||||
LOG_ERR("Unknown query %d from %s", Query, Connection->RemoteAddress);
|
||||
SendQueryStatusFailed(Connection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
420
src/database.cc
Normal file
420
src/database.cc
Normal file
@ -0,0 +1,420 @@
|
||||
#include "querymanager.hh"
|
||||
#include "sqlite3.h"
|
||||
|
||||
struct TCachedStatement{
|
||||
sqlite3_stmt *Stmt;
|
||||
int64 LastUsed;
|
||||
uint32 Hash;
|
||||
};
|
||||
|
||||
static sqlite3 *g_Database = NULL;
|
||||
static TCachedStatement *g_CachedStatements = NULL;
|
||||
|
||||
// NOTE(fusion): SQLite's application id. We're currently setting it to ASCII
|
||||
// "TiDB" for "Tibia Database".
|
||||
constexpr int g_ApplicationID = 0x54694442;
|
||||
|
||||
// Statement Cache
|
||||
//==============================================================================
|
||||
uint32 HashText(const char *Text){
|
||||
// FNV1a 32-bits
|
||||
uint32 Hash = 0x811C9DC5U;
|
||||
for(int i = 0; Text[i] != 0; i += 1){
|
||||
Hash ^= (uint32)Text[i];
|
||||
Hash *= 0x01000193U;
|
||||
}
|
||||
return Hash;
|
||||
}
|
||||
|
||||
sqlite3_stmt *PrepareQuery(const char *Text){
|
||||
sqlite3_stmt *Stmt = NULL;
|
||||
int LeastRecentlyUsed = 0;
|
||||
int64 LeastRecentlyUsedTime = g_CachedStatements[0].LastUsed;
|
||||
uint32 Hash = HashText(Text);
|
||||
for(int i = 0; i < g_MaxCachedStatements; i += 1){
|
||||
TCachedStatement *Entry = &g_CachedStatements[i];
|
||||
|
||||
if(Entry->LastUsed < LeastRecentlyUsedTime){
|
||||
LeastRecentlyUsed = i;
|
||||
LeastRecentlyUsedTime = Entry->LastUsed;
|
||||
}
|
||||
|
||||
if(Entry->Stmt != NULL && Entry->Hash == Hash){
|
||||
const char *EntryText = sqlite3_sql(Entry->Stmt);
|
||||
ASSERT(EntryText != NULL);
|
||||
if(strcmp(EntryText, Text) == 0){
|
||||
Stmt = Entry->Stmt;
|
||||
Entry->LastUsed = g_MonotonicTimeMS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(Stmt == NULL){
|
||||
if(sqlite3_prepare_v3(g_Database, Text, -1,
|
||||
SQLITE_PREPARE_PERSISTENT, &Stmt, NULL) != SQLITE_OK){
|
||||
LOG_ERR("Failed to prepary query: %s", sqlite3_errmsg(g_Database));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TCachedStatement *Entry = &g_CachedStatements[LeastRecentlyUsed];
|
||||
if(Entry->Stmt != NULL){
|
||||
sqlite3_finalize(Entry->Stmt);
|
||||
}
|
||||
|
||||
Entry->Stmt = Stmt;
|
||||
Entry->LastUsed = g_MonotonicTimeMS;
|
||||
Entry->Hash = Hash;
|
||||
}else{
|
||||
sqlite3_reset(Stmt);
|
||||
sqlite3_clear_bindings(Stmt);
|
||||
}
|
||||
|
||||
return Stmt;
|
||||
}
|
||||
|
||||
bool InitStatementCache(void){
|
||||
ASSERT(g_CachedStatements == NULL);
|
||||
g_CachedStatements = (TCachedStatement*)calloc(
|
||||
g_MaxCachedStatements, sizeof(TCachedStatement));
|
||||
return true;
|
||||
}
|
||||
|
||||
void ExitStatementCache(void){
|
||||
if(g_CachedStatements != NULL){
|
||||
for(int i = 0; i < g_MaxCachedStatements; i += 1){
|
||||
TCachedStatement *Entry = &g_CachedStatements[i];
|
||||
if(Entry->Stmt != NULL){
|
||||
sqlite3_finalize(Entry->Stmt);
|
||||
Entry->Stmt = NULL;
|
||||
}
|
||||
|
||||
Entry->LastUsed = 0;
|
||||
Entry->Hash = 0;
|
||||
}
|
||||
|
||||
free(g_CachedStatements);
|
||||
g_CachedStatements = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Queries
|
||||
//==============================================================================
|
||||
|
||||
bool LoadHouseOwners(const char *WorldName, DynamicArray<THouseOwner> *HouseOwners){
|
||||
ASSERT(WorldName && HouseOwners);
|
||||
sqlite3_stmt *Stmt = PrepareQuery(
|
||||
"SELECT HouseID, OwnerID, Characters.Name, PaidUntil"
|
||||
" FROM HouseOwners"
|
||||
" LEFT JOIN Characters ON Characters.CharacterID = OwnerID");
|
||||
if(Stmt == NULL){
|
||||
LOG_ERR("Failed to prepare query");
|
||||
return false;
|
||||
}
|
||||
|
||||
while(sqlite3_step(Stmt) == SQLITE_ROW){
|
||||
THouseOwner Owner = {};
|
||||
Owner.HouseID = sqlite3_column_int(Stmt, 0);
|
||||
Owner.OwnerID = sqlite3_column_int(Stmt, 1);
|
||||
StringCopy(Owner.OwnerName, sizeof(Owner.OwnerName),
|
||||
(const char*)sqlite3_column_text(Stmt, 2));
|
||||
Owner.PaidUntil = sqlite3_column_int(Stmt, 3);
|
||||
HouseOwners->Push(Owner);
|
||||
}
|
||||
|
||||
if(sqlite3_errcode(g_Database) != SQLITE_DONE){
|
||||
LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadWorldConfig(const char *WorldName, TWorldConfig *WorldConfig){
|
||||
ASSERT(WorldName && WorldConfig);
|
||||
sqlite3_stmt *Stmt = PrepareQuery(
|
||||
"SELECT Type, RebootTime, Address, Port, MaxPlayers,"
|
||||
" PremiumPlayerBuffer, MaxNewbies, PremiumNewbieBuffer"
|
||||
" FROM Worlds WHERE Name = ?1");
|
||||
if(Stmt == NULL){
|
||||
LOG_ERR("Failed to prepare query");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(sqlite3_bind_text(Stmt, 1, WorldName, -1, NULL) != SQLITE_OK){
|
||||
LOG_ERR("Failed to bind WorldName: %s", sqlite3_errmsg(g_Database));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(sqlite3_step(Stmt) != SQLITE_ROW){
|
||||
LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
|
||||
return false;
|
||||
}
|
||||
|
||||
WorldConfig->Type = sqlite3_column_int(Stmt, 0);
|
||||
WorldConfig->RebootTime = sqlite3_column_int(Stmt, 1);
|
||||
WorldConfig->Address = sqlite3_column_int(Stmt, 2);
|
||||
WorldConfig->Port = sqlite3_column_int(Stmt, 3);
|
||||
WorldConfig->MaxPlayers = sqlite3_column_int(Stmt, 4);
|
||||
WorldConfig->PremiumPlayerBuffer = sqlite3_column_int(Stmt, 5);
|
||||
WorldConfig->MaxNewbies = sqlite3_column_int(Stmt, 6);
|
||||
WorldConfig->PremiumNewbieBuffer = sqlite3_column_int(Stmt, 7);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Database Initialization
|
||||
//==============================================================================
|
||||
// NOTE(fusion): From `https://www.sqlite.org/pragma.html`:
|
||||
// "Some pragmas take effect during the SQL compilation stage, not the execution
|
||||
// stage. This means if using the C-language sqlite3_prepare(), sqlite3_step(),
|
||||
// sqlite3_finalize() API (or similar in a wrapper interface), the pragma may run
|
||||
// during the sqlite3_prepare() call, not during the sqlite3_step() call as normal
|
||||
// SQL statements do. Or the pragma might run during sqlite3_step() just like normal
|
||||
// SQL statements. Whether or not the pragma runs during sqlite3_prepare() or
|
||||
// sqlite3_step() depends on the pragma and on the specific release of SQLite."
|
||||
//
|
||||
// Depending on the pragma, queries will fail in the sqlite3_prepare() stage when
|
||||
// using bound parameters. This means we need to assemble the entire query before
|
||||
// hand with snprintf or other similar formatting functions. In particular, this
|
||||
// rule apply for `application_id` and `user_version` which we modify.
|
||||
|
||||
bool FileExists(const char *FileName){
|
||||
FILE *File = fopen(FileName, "rb");
|
||||
bool Result = (File != NULL);
|
||||
if(Result){
|
||||
fclose(File);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool ExecFile(const char *FileName){
|
||||
FILE *File = fopen(FileName, "rb");
|
||||
if(File == NULL){
|
||||
LOG_ERR("Failed to open file \"%s\"", FileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
fseek(File, 0, SEEK_END);
|
||||
usize FileSize = (usize)ftell(File);
|
||||
fseek(File, 0, SEEK_SET);
|
||||
|
||||
bool Result = true;
|
||||
if(FileSize > 0){
|
||||
char *Text = (char*)malloc(FileSize + 1);
|
||||
Text[FileSize] = 0;
|
||||
if(Result && fread(Text, 1, FileSize, File) != FileSize){
|
||||
LOG_ERR("Failed to read \"%s\" (ferror: %d, feof: %d)",
|
||||
FileName, ferror(File), feof(File));
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(Result && sqlite3_exec(g_Database, Text, NULL, NULL, NULL) != SQLITE_OK){
|
||||
LOG_ERR("Failed to execute \"%s\": %s",
|
||||
FileName, sqlite3_errmsg(g_Database));
|
||||
Result = false;
|
||||
}
|
||||
|
||||
free(Text);
|
||||
}
|
||||
|
||||
fclose(File);
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool ExecInternal(const char *Format, ...) ATTR_PRINTF(1, 2);
|
||||
bool ExecInternal(const char *Format, ...){
|
||||
va_list ap;
|
||||
va_start(ap, Format);
|
||||
char Text[1024];
|
||||
int Written = vsnprintf(Text, sizeof(Text), Format, ap);
|
||||
va_end(ap);
|
||||
|
||||
if(Written >= (int)sizeof(Text)){
|
||||
LOG_ERR("Query is too long");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(sqlite3_exec(g_Database, Text, NULL, NULL, NULL) != SQLITE_OK){
|
||||
LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetPragmaInt(const char *Name, int *OutValue){
|
||||
char Text[1024];
|
||||
snprintf(Text, sizeof(Text), "PRAGMA %s", Name);
|
||||
|
||||
sqlite3_stmt *Stmt;
|
||||
if(sqlite3_prepare_v2(g_Database, Text, -1, &Stmt, NULL) != SQLITE_OK){
|
||||
LOG_ERR("Failed to retrieve %s (PREP): %s", Name, sqlite3_errmsg(g_Database));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Result = (sqlite3_step(Stmt) == SQLITE_ROW);
|
||||
if(!Result){
|
||||
LOG_ERR("Failed to retrieve %s (PREP): %s", Name, sqlite3_errmsg(g_Database));
|
||||
}else if(OutValue){
|
||||
*OutValue = sqlite3_column_int(Stmt, 0);
|
||||
}
|
||||
|
||||
sqlite3_finalize(Stmt);
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool InitDatabaseSchema(void){
|
||||
bool Result = true;
|
||||
|
||||
if(Result && !ExecInternal("BEGIN")){
|
||||
LOG_ERR("Failed to start schema transaction");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(Result && !ExecFile("sql/schema.sql")){
|
||||
LOG_ERR("Failed to execute \"sql/schema.sql\"");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(Result && !ExecInternal("PRAGMA application_id = %d", g_ApplicationID)){
|
||||
LOG_ERR("Failed to set application id");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(Result && !ExecInternal("PRAGMA user_version = 1")){
|
||||
LOG_ERR("Failed to set user version");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(Result && !ExecInternal("COMMIT")){
|
||||
LOG_ERR("Failed to commit schema transaction");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(!Result && !ExecInternal("ROLLBACK")){
|
||||
LOG_ERR("Failed to rollback schema transaction");
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool UpgradeDatabaseSchema(int UserVersion){
|
||||
char FileName[256];
|
||||
int NewVersion = UserVersion;
|
||||
while(true){
|
||||
snprintf(FileName, sizeof(FileName), "sql/upgrade-%d.sql", NewVersion);
|
||||
if(FileExists(FileName)){
|
||||
NewVersion += 1;
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool Result = true;
|
||||
if(UserVersion != NewVersion){
|
||||
LOG("Upgrading database schema to version %d", NewVersion);
|
||||
|
||||
if(Result && !ExecInternal("BEGIN")){
|
||||
LOG_ERR("Failed to start upgrade transaction");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
while(Result && UserVersion < NewVersion){
|
||||
snprintf(FileName, sizeof(FileName), "upgrade-%d.sql", UserVersion);
|
||||
if(!ExecFile(FileName)){
|
||||
LOG_ERR("Failed to execute \"%s\"", FileName);
|
||||
Result = false;
|
||||
}
|
||||
UserVersion += 1;
|
||||
}
|
||||
|
||||
if(Result && !ExecInternal("PRAGMA user_version = %d", UserVersion)){
|
||||
LOG_ERR("Failed to set user version");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(Result && !ExecInternal("COMMIT")){
|
||||
LOG_ERR("Failed to commit upgrade transaction");
|
||||
Result = false;
|
||||
}
|
||||
|
||||
if(!Result && !ExecInternal("ROLLBACK")){
|
||||
LOG_ERR("Failed to rollback upgrade transaction");
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool CheckDatabaseSchema(void){
|
||||
int ApplicationID, UserVersion;
|
||||
if(!GetPragmaInt("application_id", &ApplicationID)
|
||||
|| !GetPragmaInt("user_version", &UserVersion)){
|
||||
return false;
|
||||
}
|
||||
|
||||
if(ApplicationID != g_ApplicationID){
|
||||
if(ApplicationID != 0){
|
||||
LOG_ERR("Database has unknown application id %08X (expected %08X)",
|
||||
ApplicationID, g_ApplicationID);
|
||||
return false;
|
||||
}else if(UserVersion != 0){
|
||||
LOG_ERR("Database has non zero user version %d", UserVersion);
|
||||
return false;
|
||||
}else if(!InitDatabaseSchema()){
|
||||
LOG_ERR("Failed to initialize database schema");
|
||||
return false;
|
||||
}
|
||||
|
||||
UserVersion = 1;
|
||||
}
|
||||
|
||||
if(!UpgradeDatabaseSchema(UserVersion)){
|
||||
LOG_ERR("Failed to upgrade database schema");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG("Database version: %d", UserVersion);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InitDatabase(void){
|
||||
LOG("Database file: \"%s\"", g_DatabaseFile);
|
||||
LOG("Max cached statements: %d", g_MaxCachedStatements);
|
||||
|
||||
int Flags = SQLITE_OPEN_READWRITE
|
||||
| SQLITE_OPEN_CREATE
|
||||
| SQLITE_OPEN_NOMUTEX;
|
||||
if(sqlite3_open_v2(g_DatabaseFile, &g_Database, Flags, NULL) != SQLITE_OK){
|
||||
LOG_ERR("Failed to open database at \"%s\": %s\n",
|
||||
g_DatabaseFile, sqlite3_errmsg(g_Database));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!InitStatementCache()){
|
||||
LOG_ERR("Failed to initialize statement cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!CheckDatabaseSchema()){
|
||||
LOG_ERR("Failed to check database schema");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ExitDatabase(void){
|
||||
ExitStatementCache();
|
||||
|
||||
if(g_Database != NULL){
|
||||
// NOTE(fusion): `sqlite3_close` can only fail if there are associated
|
||||
// prepared statements, blob handles, or backup objects that were not
|
||||
// finalized.
|
||||
if(sqlite3_close(g_Database) == SQLITE_OK){
|
||||
g_Database = NULL;
|
||||
}else{
|
||||
LOG_ERR("Failed to close database: %s", sqlite3_errmsg(g_Database));
|
||||
}
|
||||
}
|
||||
}
|
||||
358
src/querymanager.cc
Normal file
358
src/querymanager.cc
Normal file
@ -0,0 +1,358 @@
|
||||
#include "querymanager.hh"
|
||||
|
||||
// Time
|
||||
int g_MonotonicTimeMS = 0;
|
||||
|
||||
// Database CONFIG
|
||||
char g_DatabaseFile[1024] = "tibia.db";
|
||||
int g_MaxCachedStatements = 100;
|
||||
|
||||
// Connection CONFIG
|
||||
char g_Password[30] = "";
|
||||
int g_Port = 7174;
|
||||
int g_MaxConnections = 50;
|
||||
int g_MaxConnectionIdleTime = 60000;
|
||||
int g_MaxConnectionPacketSize = (int)MB(1);
|
||||
int g_UpdateRate = 20;
|
||||
|
||||
void LogAdd(const char *Prefix, const char *Format, ...){
|
||||
char Entry[4096];
|
||||
va_list ap;
|
||||
va_start(ap, Format);
|
||||
vsnprintf(Entry, sizeof(Entry), Format, ap);
|
||||
va_end(ap);
|
||||
|
||||
if(Entry[0] != 0){
|
||||
struct tm LocalTime = GetLocalTime(time(NULL));
|
||||
fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s\n",
|
||||
LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
|
||||
LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
|
||||
Prefix, Entry);
|
||||
}
|
||||
}
|
||||
|
||||
void LogAddVerbose(const char *Prefix, const char *Function,
|
||||
const char *File, int Line, const char *Format, ...){
|
||||
char Entry[4096];
|
||||
va_list ap;
|
||||
va_start(ap, Format);
|
||||
vsnprintf(Entry, sizeof(Entry), Format, ap);
|
||||
va_end(ap);
|
||||
|
||||
if(Entry[0] != 0){
|
||||
(void)File;
|
||||
(void)Line;
|
||||
struct tm LocalTime = GetLocalTime(time(NULL));
|
||||
fprintf(stdout, "%04d/%02d/%02d %02d:%02d:%02d [%s] %s: %s\n",
|
||||
LocalTime.tm_year + 1900, LocalTime.tm_mon + 1, LocalTime.tm_mday,
|
||||
LocalTime.tm_hour, LocalTime.tm_min, LocalTime.tm_sec,
|
||||
Prefix, Function, Entry);
|
||||
}
|
||||
}
|
||||
|
||||
struct tm GetLocalTime(time_t t){
|
||||
struct tm result;
|
||||
#if COMPILER_MSVC
|
||||
localtime_s(&result, &t);
|
||||
#else
|
||||
localtime_r(&t, &result);
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
int64 GetClockMonotonicMS(void){
|
||||
#if OS_WINDOWS
|
||||
LARGE_INTEGER Counter, Frequency;
|
||||
QueryPerformanceCounter(&Counter);
|
||||
QueryPerformanceFrequency(&Frequency);
|
||||
return (int64)((Counter.QuadPart * 1000) / Frequency.QuadPart);
|
||||
#else
|
||||
struct timespec Time;
|
||||
clock_gettime(CLOCK_MONOTONIC, &Time);
|
||||
return ((int64)Time.tv_sec * 1000)
|
||||
+ ((int64)Time.tv_nsec / 1000000);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SleepMS(int64 DurationMS){
|
||||
#if OS_WINDOWS
|
||||
Sleep((DWORD)DurationMS);
|
||||
#else
|
||||
struct timespec Duration;
|
||||
Duration.tv_sec = (time_t)(DurationMS / 1000);
|
||||
Duration.tv_nsec = (long)((DurationMS % 1000) * 1000000);
|
||||
nanosleep(&Duration, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool StringEq(const char *A, const char *B){
|
||||
int Index = 0;
|
||||
while(true){
|
||||
if(A[Index] != B[Index]){
|
||||
return false;
|
||||
}else if(A[Index] == 0){
|
||||
return true;
|
||||
}
|
||||
Index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool StringEqCI(const char *A, const char *B){
|
||||
int Index = 0;
|
||||
while(true){
|
||||
if(tolower(A[Index]) != tolower(B[Index])){
|
||||
return false;
|
||||
}else if(A[Index] == 0){
|
||||
return true;
|
||||
}
|
||||
Index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength){
|
||||
ASSERT(DestCapacity > 0);
|
||||
bool Result = (SrcLength < DestCapacity);
|
||||
if(Result){
|
||||
memcpy(Dest, Src, SrcLength);
|
||||
Dest[SrcLength] = 0;
|
||||
}else{
|
||||
Dest[0] = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StringCopy(char *Dest, int DestCapacity, const char *Src){
|
||||
return StringCopyN(Dest, DestCapacity, Src, (int)strlen(Src));
|
||||
}
|
||||
|
||||
bool ReadBooleanConfig(bool *Dest, const char *Val){
|
||||
ASSERT(Dest && Val);
|
||||
*Dest = StringEqCI(Val, "true");
|
||||
return *Dest || StringEqCI(Val, "false");
|
||||
}
|
||||
|
||||
bool ReadIntegerConfig(int *Dest, const char *Val){
|
||||
ASSERT(Dest && Val);
|
||||
const char *ValEnd;
|
||||
*Dest = (int)strtol(Val, (char**)&ValEnd, 0);
|
||||
return ValEnd > Val;
|
||||
}
|
||||
|
||||
bool ReadDurationConfig(int *Dest, const char *Val){
|
||||
ASSERT(Dest && Val);
|
||||
const char *Suffix;
|
||||
*Dest = (int)strtol(Val, (char**)&Suffix, 0);
|
||||
if(Suffix == Val){
|
||||
return false;
|
||||
}
|
||||
|
||||
while(Suffix[0] != 0 && isspace(Suffix[0])){
|
||||
Suffix += 1;
|
||||
}
|
||||
|
||||
if(Suffix[0] == 'S' || Suffix[0] == 's'){
|
||||
*Dest *= (1000);
|
||||
}else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
|
||||
*Dest *= (60 * 1000);
|
||||
}else if(Suffix[0] == 'H' || Suffix[0] == 'h'){
|
||||
*Dest *= (60 * 60 * 1000);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadSizeConfig(int *Dest, const char *Val){
|
||||
ASSERT(Dest && Val);
|
||||
const char *Suffix;
|
||||
*Dest = (int)strtol(Val, (char**)&Suffix, 0);
|
||||
if(Suffix == Val){
|
||||
return false;
|
||||
}
|
||||
|
||||
while(Suffix[0] != 0 && isspace(Suffix[0])){
|
||||
Suffix += 1;
|
||||
}
|
||||
|
||||
if(Suffix[0] == 'K' || Suffix[0] == 'k'){
|
||||
*Dest *= (1024);
|
||||
}else if(Suffix[0] == 'M' || Suffix[0] == 'm'){
|
||||
*Dest *= (1024 * 1024);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadStringConfig(char *Dest, int DestCapacity, const char *Val){
|
||||
ASSERT(Dest && DestCapacity > 0 && Val);
|
||||
int ValStart = 0;
|
||||
int ValEnd = (int)strlen(Val);
|
||||
if(ValEnd >= 2){
|
||||
if((Val[0] == '"' && Val[ValEnd - 1] == '"')
|
||||
|| (Val[0] == '\'' && Val[ValEnd - 1] == '\'')
|
||||
|| (Val[0] == '`' && Val[ValEnd - 1] == '`')){
|
||||
ValStart += 1;
|
||||
ValEnd -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return StringCopyN(Dest, DestCapacity,
|
||||
&Val[ValStart], (ValEnd - ValStart));
|
||||
}
|
||||
|
||||
bool ReadConfig(const char *FileName){
|
||||
FILE *File = fopen(FileName, "rb");
|
||||
if(File == NULL){
|
||||
LOG_ERR("Failed to open config file \"%s\"", FileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EndOfFile = false;
|
||||
for(int LineNumber = 1; !EndOfFile; LineNumber += 1){
|
||||
char Line[1024];
|
||||
int MaxLineSize = (int)sizeof(Line);
|
||||
int LineSize = 0;
|
||||
int KeyStart = -1;
|
||||
int EqualPos = -1;
|
||||
while(true){
|
||||
int ch = fgetc(File);
|
||||
if(ch == EOF || ch == '\n'){
|
||||
if(ch == EOF){
|
||||
EndOfFile = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(LineSize < MaxLineSize){
|
||||
Line[LineSize] = (char)ch;
|
||||
}
|
||||
|
||||
if(KeyStart == -1 && !isspace(ch)){
|
||||
KeyStart = LineSize;
|
||||
}
|
||||
|
||||
if(EqualPos == -1 && ch == '='){
|
||||
EqualPos = LineSize;
|
||||
}
|
||||
|
||||
LineSize += 1;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Check line size limit.
|
||||
if(LineSize > MaxLineSize){
|
||||
LOG_WARN("%s:%d: Exceeded line size limit of %d characters",
|
||||
FileName, LineNumber, MaxLineSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Check empty line or comment.
|
||||
if(KeyStart == -1 || Line[KeyStart] == '#'){
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Check assignment.
|
||||
if(EqualPos == -1){
|
||||
LOG_WARN("%s:%d: No assignment found on non empty line",
|
||||
FileName, LineNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Check empty key.
|
||||
int KeyEnd = EqualPos;
|
||||
while(KeyEnd > KeyStart && isspace(Line[KeyEnd - 1])){
|
||||
KeyEnd -= 1;
|
||||
}
|
||||
|
||||
if(KeyStart == KeyEnd){
|
||||
LOG_WARN("%s:%d: Empty key", FileName, LineNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Check empty value.
|
||||
int ValStart = EqualPos + 1;
|
||||
int ValEnd = LineSize;
|
||||
while(ValStart < ValEnd && isspace(Line[ValStart])){
|
||||
ValStart += 1;
|
||||
}
|
||||
|
||||
while(ValEnd > ValStart && isspace(Line[ValEnd - 1])){
|
||||
ValEnd -= 1;
|
||||
}
|
||||
|
||||
if(ValStart == ValEnd){
|
||||
LOG_WARN("%s:%d: Empty value", FileName, LineNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Parse KV pair.
|
||||
char Key[256];
|
||||
if(!StringCopyN(Key, (int)sizeof(Key), &Line[KeyStart], (KeyEnd - KeyStart))){
|
||||
LOG_WARN("%s:%d: Exceeded key size limit of %d characters",
|
||||
FileName, LineNumber, (int)(sizeof(Key) - 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
char Val[256];
|
||||
if(!StringCopyN(Val, (int)sizeof(Val), &Line[ValStart], (ValEnd - ValStart))){
|
||||
LOG_WARN("%s:%d: Exceeded value size limit of %d characters",
|
||||
FileName, LineNumber, (int)(sizeof(Val) - 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if(StringEqCI(Key, "DatabaseFile")){
|
||||
ReadStringConfig(g_DatabaseFile, (int)sizeof(g_DatabaseFile), Val);
|
||||
}else if(StringEqCI(Key, "MaxCachedStatements")){
|
||||
ReadIntegerConfig(&g_MaxCachedStatements, Val);
|
||||
}else if(StringEqCI(Key, "Password")){
|
||||
ReadStringConfig(g_Password, (int)sizeof(g_Password), Val);
|
||||
}else if(StringEqCI(Key, "Port")){
|
||||
ReadIntegerConfig(&g_Port, Val);
|
||||
}else if(StringEqCI(Key, "MaxConnections")){
|
||||
ReadIntegerConfig(&g_MaxConnections, Val);
|
||||
}else if(StringEqCI(Key, "MaxConnectionIdleTime")){
|
||||
ReadDurationConfig(&g_MaxConnectionIdleTime, Val);
|
||||
}else if(StringEqCI(Key, "MaxConnectionPacketSize")){
|
||||
ReadSizeConfig(&g_MaxConnectionPacketSize, Val);
|
||||
}else if(StringEqCI(Key, "UpdateRate")){
|
||||
ReadIntegerConfig(&g_UpdateRate, Val);
|
||||
}else{
|
||||
LOG_WARN("Unknown config \"%s\"", Key);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(File);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv){
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
int64 StartTime = GetClockMonotonicMS();
|
||||
g_MonotonicTimeMS = 0;
|
||||
|
||||
LOG("Tibia Query Manager v0.1");
|
||||
if(!ReadConfig("config.cfg")){
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
atexit(ExitDatabase);
|
||||
atexit(ExitConnections);
|
||||
if(!InitDatabase() || !InitConnections()){
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
LOG("Running at %d updates per second...", g_UpdateRate);
|
||||
int64 UpdateInterval = 1000 / (int64)g_UpdateRate;
|
||||
while(true){
|
||||
int64 UpdateStart = GetClockMonotonicMS();
|
||||
g_MonotonicTimeMS = (int)(UpdateStart - StartTime);
|
||||
ProcessConnections();
|
||||
int64 UpdateEnd = GetClockMonotonicMS();
|
||||
int64 NextUpdate = UpdateStart + UpdateInterval;
|
||||
if(NextUpdate > UpdateEnd){
|
||||
SleepMS(NextUpdate - UpdateEnd);
|
||||
}
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
669
src/querymanager.hh
Normal file
669
src/querymanager.hh
Normal file
@ -0,0 +1,669 @@
|
||||
#ifndef TIBIA_QUERYMANAGER_HH_
|
||||
#define TIBIA_QUERYMANAGER_HH_ 1
|
||||
|
||||
#include <ctype.h>
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
typedef uint8_t uint8;
|
||||
typedef uint16_t uint16;
|
||||
typedef uint32_t uint32;
|
||||
typedef int64_t int64;
|
||||
typedef uint64_t uint64;
|
||||
typedef size_t usize;
|
||||
|
||||
#define STATIC_ASSERT(expr) static_assert((expr), "static assertion failed: " #expr)
|
||||
#define NARRAY(arr) (int)(sizeof(arr) / sizeof(arr[0]))
|
||||
#define ISPOW2(x) ((x) != 0 && ((x) & ((x) - 1)) == 0)
|
||||
#define KB(x) ((usize)(x) << 10)
|
||||
#define MB(x) ((usize)(x) << 20)
|
||||
#define GB(x) ((usize)(x) << 30)
|
||||
|
||||
#if defined(_WIN32)
|
||||
# define OS_WINDOWS 1
|
||||
#elif defined(__linux__) || defined(__gnu_linux__)
|
||||
# define OS_LINUX 1
|
||||
#else
|
||||
# error "Operating system not supported."
|
||||
#endif
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
# define COMPILER_MSVC 1
|
||||
#elif defined(__GNUC__)
|
||||
# define COMPILER_GCC 1
|
||||
#elif defined(__clang__)
|
||||
# define COMPILER_CLANG 1
|
||||
#endif
|
||||
|
||||
#if COMPILER_GCC || COMPILER_CLANG
|
||||
# define ATTR_FALLTHROUGH __attribute__((fallthrough))
|
||||
# define ATTR_PRINTF(x, y) __attribute__((format(printf, x, y)))
|
||||
#else
|
||||
# define ATTR_FALLTHROUGH
|
||||
# define ATTR_PRINTF(x, y)
|
||||
#endif
|
||||
|
||||
#if COMPILER_MSVC
|
||||
# define TRAP() __debugbreak()
|
||||
#elif COMPILER_GCC || COMPILER_CLANG
|
||||
# define TRAP() __builtin_trap()
|
||||
#else
|
||||
# define TRAP() abort()
|
||||
#endif
|
||||
|
||||
#define ASSERT_ALWAYS(expr) if(!(expr)) { TRAP(); }
|
||||
#if BUILD_DEBUG
|
||||
# define ASSERT(expr) ASSERT_ALWAYS(expr)
|
||||
#else
|
||||
# define ASSERT(expr) ((void)(expr))
|
||||
#endif
|
||||
|
||||
#define LOG(...) LogAdd("INFO", __VA_ARGS__)
|
||||
#define LOG_WARN(...) LogAddVerbose("WARN", __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
|
||||
#define LOG_ERR(...) LogAddVerbose("ERR", __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__)
|
||||
#define PANIC(...) \
|
||||
do{ \
|
||||
LogAddVerbose("PANIC", __FUNCTION__, __FILE__, __LINE__, __VA_ARGS__); \
|
||||
TRAP(); \
|
||||
}while(0)
|
||||
|
||||
// Time
|
||||
extern int g_MonotonicTimeMS;
|
||||
|
||||
// Database CONFIG
|
||||
extern char g_DatabaseFile[1024];
|
||||
extern int g_MaxCachedStatements;
|
||||
|
||||
// Connection CONFIG
|
||||
extern char g_Password[30];
|
||||
extern int g_Port;
|
||||
extern int g_MaxConnections;
|
||||
extern int g_MaxConnectionIdleTime;
|
||||
extern int g_MaxConnectionPacketSize;
|
||||
extern int g_UpdateRate;
|
||||
|
||||
void LogAdd(const char *Prefix, const char *Format, ...) ATTR_PRINTF(2, 3);
|
||||
void LogAddVerbose(const char *Prefix, const char *Function,
|
||||
const char *File, int Line, const char *Format, ...) ATTR_PRINTF(5, 6);
|
||||
|
||||
struct tm GetLocalTime(time_t t);
|
||||
int64 GetClockMonotonicMS(void);
|
||||
void SleepMS(int64 DurationMS);
|
||||
|
||||
bool StringEq(const char *A, const char *B);
|
||||
bool StringEqCI(const char *A, const char *B);
|
||||
bool StringCopyN(char *Dest, int DestCapacity, const char *Src, int SrcLength);
|
||||
bool StringCopy(char *Dest, int DestCapacity, const char *Src);
|
||||
|
||||
bool ReadBooleanConfig(bool *Dest, const char *Val);
|
||||
bool ReadIntegerConfig(int *Dest, const char *Val);
|
||||
bool ReadSizeConfig(int *Dest, const char *Val);
|
||||
bool ReadStringConfig(char *Dest, int DestCapacity, const char *Val);
|
||||
bool ReadConfig(const char *FileName);
|
||||
|
||||
// Buffer Utility
|
||||
//==============================================================================
|
||||
inline uint8 BufferRead8(const uint8 *Buffer){
|
||||
return Buffer[0];
|
||||
}
|
||||
|
||||
inline uint16 BufferRead16LE(const uint8 *Buffer){
|
||||
return (uint16)Buffer[0]
|
||||
| ((uint16)Buffer[1] << 8);
|
||||
}
|
||||
|
||||
inline uint16 BufferRead16BE(const uint8 *Buffer){
|
||||
return ((uint16)Buffer[0] << 8)
|
||||
| (uint16)Buffer[1];
|
||||
}
|
||||
|
||||
inline uint32 BufferRead32LE(const uint8 *Buffer){
|
||||
return (uint32)Buffer[0]
|
||||
| ((uint32)Buffer[1] << 8)
|
||||
| ((uint32)Buffer[2] << 16)
|
||||
| ((uint32)Buffer[3] << 24);
|
||||
}
|
||||
|
||||
inline uint32 BufferRead32BE(const uint8 *Buffer){
|
||||
return ((uint32)Buffer[0] << 24)
|
||||
| ((uint32)Buffer[1] << 16)
|
||||
| ((uint32)Buffer[2] << 8)
|
||||
| (uint32)Buffer[3];
|
||||
}
|
||||
|
||||
inline void BufferWrite8(uint8 *Buffer, uint8 Value){
|
||||
Buffer[0] = Value;
|
||||
}
|
||||
|
||||
inline void BufferWrite16LE(uint8 *Buffer, uint16 Value){
|
||||
Buffer[0] = (uint8)(Value >> 0);
|
||||
Buffer[1] = (uint8)(Value >> 8);
|
||||
}
|
||||
|
||||
inline void BufferWrite16BE(uint8 *Buffer, uint16 Value){
|
||||
Buffer[0] = (uint8)(Value >> 8);
|
||||
Buffer[1] = (uint8)(Value >> 0);
|
||||
}
|
||||
|
||||
inline void BufferWrite32LE(uint8 *Buffer, uint32 Value){
|
||||
Buffer[0] = (uint8)(Value >> 0);
|
||||
Buffer[1] = (uint8)(Value >> 8);
|
||||
Buffer[2] = (uint8)(Value >> 16);
|
||||
Buffer[3] = (uint8)(Value >> 24);
|
||||
}
|
||||
|
||||
inline void BufferWrite32BE(uint8 *Buffer, uint32 Value){
|
||||
Buffer[0] = (uint8)(Value >> 24);
|
||||
Buffer[1] = (uint8)(Value >> 16);
|
||||
Buffer[2] = (uint8)(Value >> 8);
|
||||
Buffer[3] = (uint8)(Value >> 0);
|
||||
}
|
||||
|
||||
struct TReadBuffer{
|
||||
TReadBuffer(uint8 *Buffer, int Size)
|
||||
: Buffer(Buffer), Size(Size), Position(0) {}
|
||||
|
||||
bool CanRead(int Bytes){
|
||||
return (this->Position + Bytes) <= this->Size;
|
||||
}
|
||||
|
||||
bool Overflowed(void){
|
||||
return this->Position > this->Size;
|
||||
}
|
||||
|
||||
uint8 Read8(void){
|
||||
uint8 Result = 0;
|
||||
if(this->CanRead(1)){
|
||||
Result = BufferRead8(this->Buffer + this->Position);
|
||||
}
|
||||
this->Position += 1;
|
||||
return Result;
|
||||
}
|
||||
|
||||
uint16 Read16(void){
|
||||
uint16 Result = 0;
|
||||
if(this->CanRead(2)){
|
||||
Result = BufferRead16LE(this->Buffer + this->Position);
|
||||
}
|
||||
this->Position += 2;
|
||||
return Result;
|
||||
}
|
||||
|
||||
uint16 Read16BE(void){
|
||||
uint16 Result = 0;
|
||||
if(this->CanRead(2)){
|
||||
Result = BufferRead16BE(this->Buffer + this->Position);
|
||||
}
|
||||
this->Position += 2;
|
||||
return Result;
|
||||
}
|
||||
|
||||
uint32 Read32(void){
|
||||
uint32 Result = 0;
|
||||
if(this->CanRead(4)){
|
||||
Result = BufferRead32LE(this->Buffer + this->Position);
|
||||
}
|
||||
this->Position += 4;
|
||||
return Result;
|
||||
}
|
||||
|
||||
uint32 Read32BE(void){
|
||||
uint32 Result = 0;
|
||||
if(this->CanRead(4)){
|
||||
Result = BufferRead32BE(this->Buffer + this->Position);
|
||||
}
|
||||
this->Position += 4;
|
||||
return Result;
|
||||
}
|
||||
|
||||
void ReadString(char *Dest, int DestCapacity){
|
||||
int Length = (int)this->Read16();
|
||||
if(Length == 0xFFFF){
|
||||
Length = (int)this->Read32();
|
||||
}
|
||||
|
||||
if(Dest != NULL && DestCapacity > 0){
|
||||
if(Length < DestCapacity && this->CanRead(Length)){
|
||||
memcpy(Dest, this->Buffer + this->Position, Length);
|
||||
Dest[Length] = 0;
|
||||
}else{
|
||||
Dest[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this->Position += Length;
|
||||
}
|
||||
|
||||
// DATA
|
||||
// =================
|
||||
uint8 *Buffer;
|
||||
int Size;
|
||||
int Position;
|
||||
};
|
||||
|
||||
struct TWriteBuffer{
|
||||
TWriteBuffer(uint8 *Buffer, int Size)
|
||||
: Buffer(Buffer), Size(Size), Position(0) {}
|
||||
|
||||
bool CanWrite(int Bytes){
|
||||
return (this->Position + Bytes) <= this->Size;
|
||||
}
|
||||
|
||||
bool Overflowed(void){
|
||||
return this->Position > this->Size;
|
||||
}
|
||||
|
||||
void Write8(uint8 Value){
|
||||
if(this->CanWrite(1)){
|
||||
BufferWrite8(this->Buffer + this->Position, Value);
|
||||
}
|
||||
this->Position += 1;
|
||||
}
|
||||
|
||||
void Write16(uint16 Value){
|
||||
if(this->CanWrite(2)){
|
||||
BufferWrite16LE(this->Buffer + this->Position, Value);
|
||||
}
|
||||
this->Position += 2;
|
||||
}
|
||||
|
||||
void Write16BE(uint16 Value){
|
||||
if(this->CanWrite(2)){
|
||||
BufferWrite16BE(this->Buffer + this->Position, Value);
|
||||
}
|
||||
this->Position += 2;
|
||||
}
|
||||
|
||||
void Write32(uint32 Value){
|
||||
if(this->CanWrite(4)){
|
||||
BufferWrite32LE(this->Buffer + this->Position, Value);
|
||||
}
|
||||
this->Position += 4;
|
||||
}
|
||||
|
||||
void Write32BE(uint32 Value){
|
||||
if(this->CanWrite(4)){
|
||||
BufferWrite32BE(this->Buffer + this->Position, Value);
|
||||
}
|
||||
this->Position += 4;
|
||||
}
|
||||
|
||||
void WriteString(const char *String){
|
||||
int StringLength = 0;
|
||||
if(String != NULL){
|
||||
StringLength = (int)strlen(String);
|
||||
}
|
||||
|
||||
if(StringLength < 0xFFFF){
|
||||
this->Write16((uint16)StringLength);
|
||||
}else{
|
||||
this->Write16(0xFFFF);
|
||||
this->Write32((uint32)StringLength);
|
||||
}
|
||||
|
||||
if(StringLength > 0 && this->CanWrite(StringLength)){
|
||||
memcpy(this->Buffer + this->Position, String, StringLength);
|
||||
}
|
||||
|
||||
this->Position += StringLength;
|
||||
}
|
||||
|
||||
void Rewrite16(int Position, uint16 Value){
|
||||
if((Position + 2) <= this->Position){
|
||||
BufferWrite16LE(this->Buffer + Position, Value);
|
||||
}
|
||||
}
|
||||
|
||||
void Insert32(int Position, uint32 Value){
|
||||
if(Position <= this->Position){
|
||||
if(this->CanWrite(4)){
|
||||
memmove(this->Buffer + Position + 4,
|
||||
this->Buffer + Position,
|
||||
this->Position - Position);
|
||||
BufferWrite32LE(this->Buffer + Position, Value);
|
||||
}
|
||||
|
||||
this->Position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// DATA
|
||||
// =================
|
||||
uint8 *Buffer;
|
||||
int Size;
|
||||
int Position;
|
||||
};
|
||||
|
||||
// Dynamic Array
|
||||
//==============================================================================
|
||||
template<typename T>
|
||||
struct DynamicArray{
|
||||
private:
|
||||
// IMPORTANT(fusion): This container is meant to be used with POD types.
|
||||
// Using it with anything else would most likely cause problems.
|
||||
STATIC_ASSERT(std::is_trivially_default_constructible<T>::value
|
||||
&& std::is_trivially_destructible<T>::value
|
||||
&& std::is_trivially_copyable<T>::value);
|
||||
|
||||
T *m_Data;
|
||||
int m_Length;
|
||||
int m_Capacity;
|
||||
|
||||
void EnsureCapacity(int Capacity){
|
||||
int OldCapacity = m_Capacity;
|
||||
if(Capacity > OldCapacity){
|
||||
// NOTE(fusion): Exponentially grow backing array.
|
||||
int NewCapacity = (OldCapacity > 0 ? OldCapacity : 8);
|
||||
while(NewCapacity < Capacity){
|
||||
if(NewCapacity > (INT_MAX - (NewCapacity / 2))){
|
||||
NewCapacity = INT_MAX;
|
||||
break;
|
||||
}
|
||||
|
||||
NewCapacity += (NewCapacity / 2);
|
||||
}
|
||||
ASSERT(NewCapacity >= Capacity);
|
||||
|
||||
T *NewData = (T*)realloc(m_Data, sizeof(T) * (usize)NewCapacity);
|
||||
if(NewData == NULL){
|
||||
PANIC("Failed to resize dynamic array from %d to %d", OldCapacity, NewCapacity);
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Zero initialize newly allocated elements.
|
||||
memset(&NewData[OldCapacity], 0, sizeof(T) * (usize)(NewCapacity - OldCapacity));
|
||||
|
||||
m_Data = NewData;
|
||||
m_Capacity = NewCapacity;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
DynamicArray(void) : m_Data(NULL), m_Length(0), m_Capacity(0) {}
|
||||
~DynamicArray(void){
|
||||
if(m_Data != NULL){
|
||||
free(m_Data);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(fusion): Make it non copyable for simplicity. Implementing copy and
|
||||
// move operations could be useful on a general context but it won't make a
|
||||
// difference here since we're not gonna use it.
|
||||
DynamicArray(const DynamicArray &Other) = delete;
|
||||
void operator=(const DynamicArray &Other) = delete;
|
||||
|
||||
bool Empty(void) const { return m_Length == 0; }
|
||||
int Length(void) const { return m_Length; }
|
||||
int Capacity(void) const { return m_Capacity; }
|
||||
|
||||
void Reserve(int Capacity){
|
||||
EnsureCapacity(Capacity);
|
||||
}
|
||||
|
||||
void Resize(int Length){
|
||||
ASSERT(Length >= 0);
|
||||
EnsureCapacity(Length);
|
||||
if(Length < m_Length){
|
||||
// NOTE(fusion): Maintain non-active elements zero initialized.
|
||||
memset(&m_Data[Length], 0, sizeof(T) * (usize)(m_Length - Length));
|
||||
}
|
||||
|
||||
m_Length = Length;
|
||||
}
|
||||
|
||||
void Insert(int Index, const T &Element){
|
||||
ASSERT(Index >= 0 && Index <= m_Length);
|
||||
EnsureCapacity(m_Length + 1);
|
||||
for(int i = m_Length; i > Index; i -= 1){
|
||||
m_Data[i] = m_Data[i - 1];
|
||||
}
|
||||
m_Data[Index] = Element;
|
||||
m_Length += 1;
|
||||
}
|
||||
|
||||
void Push(const T &Element){
|
||||
EnsureCapacity(m_Length + 1);
|
||||
m_Data[m_Length] = Element;
|
||||
m_Length += 1;
|
||||
}
|
||||
|
||||
void Remove(int Index){
|
||||
ASSERT(Index >= 0 && Index < m_Length);
|
||||
m_Length -= 1;
|
||||
for(int i = Index; i < m_Length; i += 1){
|
||||
m_Data[i] = m_Data[i + 1];
|
||||
}
|
||||
// NOTE(fusion): Maintain non-active elements zero initialized.
|
||||
memset(&m_Data[m_Length], 0, sizeof(T));
|
||||
}
|
||||
|
||||
void Pop(void){
|
||||
ASSERT(m_Length > 0);
|
||||
m_Length -= 1;
|
||||
// NOTE(fusion): Maintain non-active elements zero initialized.
|
||||
memset(&m_Data[m_Length], 0, sizeof(T));
|
||||
}
|
||||
|
||||
void SwapAndPop(int Index){
|
||||
ASSERT(Index >= 0 && Index < m_Length);
|
||||
m_Length -= 1;
|
||||
m_Data[Index] = m_Data[m_Length];
|
||||
// NOTE(fusion): Maintain non-active elements zero initialized.
|
||||
memset(&m_Data[m_Length], 0, sizeof(T));
|
||||
}
|
||||
|
||||
T &operator[](int Index){
|
||||
ASSERT(Index >= 0 && Index < m_Length);
|
||||
return m_Data[Index];
|
||||
}
|
||||
|
||||
const T &operator[](int Index) const {
|
||||
ASSERT(Index >= 0 && Index < m_Length);
|
||||
return m_Data[Index];
|
||||
}
|
||||
|
||||
// ranged for loop
|
||||
T *begin(void) { return m_Data; }
|
||||
T *end(void) { return m_Data + m_Length; }
|
||||
const T *begin(void) const { return m_Data; }
|
||||
const T *end(void) const { return m_Data + m_Length; }
|
||||
};
|
||||
|
||||
// connections.cc
|
||||
//==============================================================================
|
||||
enum : int {
|
||||
APPLICATION_TYPE_GAME = 1,
|
||||
|
||||
// TODO(fusion): There is definitely more application types that we don't
|
||||
// know about. From looking at the config loader on the server application,
|
||||
// we could perhaps infer a few, although we're gonna be limited to our own
|
||||
// usage cases (GAME and WEB).
|
||||
//APPLICATION_TYPE_WEB
|
||||
//APPLICATION_TYPE_FORUM
|
||||
//APPLICATION_TYPE_ADMIN
|
||||
};
|
||||
|
||||
enum : int {
|
||||
QUERY_STATUS_OK = 0,
|
||||
QUERY_STATUS_ERROR = 1,
|
||||
QUERY_STATUS_FAILED = 3,
|
||||
};
|
||||
|
||||
enum : int {
|
||||
QUERY_LOGIN = 0,
|
||||
QUERY_CHECK_ACCOUNT_PASSWORD = 10,
|
||||
QUERY_LOGIN_ADMIN = 12,
|
||||
QUERY_LOGIN_GAME = 20,
|
||||
QUERY_LOGOUT_GAME = 21,
|
||||
QUERY_SET_NAMELOCK = 23,
|
||||
QUERY_BANISH_ACCOUNT = 25,
|
||||
QUERY_SET_NOTATION = 26,
|
||||
QUERY_REPORT_STATEMENT = 27,
|
||||
QUERY_BANISH_IP_ADDRESS = 28,
|
||||
QUERY_LOG_CHARACTER_DEATH = 29,
|
||||
QUERY_ADD_BUDDY = 30,
|
||||
QUERY_REMOVE_BUDDY = 31,
|
||||
QUERY_DECREMENT_IS_ONLINE = 32,
|
||||
QUERY_FINISH_AUCTIONS = 33,
|
||||
QUERY_TRANSFER_HOUSES = 35,
|
||||
QUERY_EVICT_FREE_ACCOUNTS = 36,
|
||||
QUERY_EVICT_DELETED_CHARACTERS = 37,
|
||||
QUERY_EVICT_EX_GUILDLEADERS = 38,
|
||||
QUERY_INSERT_HOUSE_OWNER = 39,
|
||||
QUERY_UPDATE_HOUSE_OWNER = 40,
|
||||
QUERY_DELETE_HOUSE_OWNER = 41,
|
||||
QUERY_GET_HOUSE_OWNERS = 42,
|
||||
QUERY_GET_AUCTIONS = 43,
|
||||
QUERY_START_AUCTION = 44,
|
||||
QUERY_INSERT_HOUSES = 45,
|
||||
QUERY_CLEAR_IS_ONLINE = 46,
|
||||
QUERY_CREATE_PLAYERLIST = 47,
|
||||
QUERY_LOG_KILLED_CREATURES = 48,
|
||||
QUERY_LOAD_PLAYERS = 50,
|
||||
QUERY_EXCLUDE_FROM_AUCTIONS = 51,
|
||||
QUERY_CANCEL_HOUSE_TRANSFER = 52,
|
||||
QUERY_LOAD_WORLD_CONFIG = 53,
|
||||
QUERY_GET_KEPT_CHARACTERS = 200,
|
||||
QUERY_GET_DELETED_CHARACTERS = 201,
|
||||
QUERY_DELETE_OLD_CHARACTER = 202,
|
||||
QUERY_GET_HIDDEN_CHARACTERS = 203,
|
||||
QUERY_CREATE_HIGHSCORES = 204,
|
||||
QUERY_CREATE_CENSUS = 205,
|
||||
QUERY_CREATE_KILL_STATISTICS = 206,
|
||||
QUERY_GET_PLAYERS_ONLINE = 207,
|
||||
QUERY_GET_WORLDS = 208,
|
||||
QUERY_GET_SERVER_LOAD = 209,
|
||||
QUERY_INSERT_PAYMENT_DATA_OLD = 210,
|
||||
QUERY_ADD_PAYMENT_OLD = 211,
|
||||
QUERY_CANCEL_PAYMENT_OLD = 212,
|
||||
QUERY_INSERT_PAYMENT_DATA_NEW = 213,
|
||||
QUERY_ADD_PAYMENT_NEW = 214,
|
||||
QUERY_CANCEL_PAYMENT_NEW = 215,
|
||||
};
|
||||
|
||||
enum ConnectionState: int {
|
||||
CONNECTION_FREE = 0,
|
||||
CONNECTION_READING = 1,
|
||||
CONNECTION_PROCESSING = 2,
|
||||
CONNECTION_WRITING = 3,
|
||||
};
|
||||
|
||||
struct TConnection{
|
||||
ConnectionState State;
|
||||
int Socket;
|
||||
int LastActive;
|
||||
int RWSize;
|
||||
int RWPosition;
|
||||
uint8 *Buffer;
|
||||
bool Authorized;
|
||||
int ApplicationType;
|
||||
char ApplicationData[30];
|
||||
char RemoteAddress[30];
|
||||
};
|
||||
|
||||
int ListenerBind(uint16 Port);
|
||||
int ListenerAccept(int Listener, uint32 *OutAddr, uint16 *OutPort);
|
||||
void CloseConnection(TConnection *Connection);
|
||||
void EnsureConnectionBuffer(TConnection *Connection);
|
||||
void DeleteConnectionBuffer(TConnection *Connection);
|
||||
TConnection *AssignConnection(int Socket, uint32 Addr, uint16 Port);
|
||||
void ReleaseConnection(TConnection *Connection);
|
||||
void CheckConnectionInput(TConnection *Connection, int Events);
|
||||
void CheckConnectionOutput(TConnection *Connection, int Events);
|
||||
void CheckConnection(TConnection *Connection, int Events);
|
||||
void ProcessConnections(void);
|
||||
bool InitConnections(void);
|
||||
void ExitConnections(void);
|
||||
|
||||
TWriteBuffer PrepareResponse(TConnection *Connection, int Status);
|
||||
void SendResponse(TConnection *Connection, TWriteBuffer *WriteBuffer);
|
||||
void SendQueryStatusOk(TConnection *Connection);
|
||||
void SendQueryStatusError(TConnection *Connection, int ErrorCode);
|
||||
void SendQueryStatusFailed(TConnection *Connection);
|
||||
void ProcessLoginQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCheckAccountPasswordQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessLoginAdminQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessLoginGameQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessLogoutGameQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessSetNamelockQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessBanishAccountQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessSetNotationQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessReportStatementQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessBanishIpAddressQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessLogCharacterDeathQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessAddBuddyQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessRemoveBuddyQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessDecrementIsOnlineQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessFinishAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessTransferHousesQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessEvictFreeAccountsQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessEvictDeletedCharactersQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessEvictExGuildleadersQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessInsertHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessUpdateHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessDeleteHouseOwnerQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetHouseOwnersQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessStartAuctionQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessInsertHousesQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessClearIsOnlineQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCreatePlayerlistQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessLogKilledCreaturesQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessLoadPlayersQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessExcludeFromAuctionsQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCancelHouseTransferQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessLoadWorldConfigQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetKeptCharactersQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetDeletedCharactersQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessDeleteOldCharacterQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetHiddenCharactersQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCreateHighscoresQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCreateCensusQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCreateKillStatisticsQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetPlayersOnlineQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetWorldsQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessGetServerLoadQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessInsertPaymentDataOldQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessAddPaymentOldQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCancelPaymentOldQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessInsertPaymentDataNewQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessAddPaymentNewQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessCancelPaymentNewQuery(TConnection *Connection, TReadBuffer *Buffer);
|
||||
void ProcessConnectionQuery(TConnection *Connection);
|
||||
|
||||
|
||||
// database.cc
|
||||
//==============================================================================
|
||||
struct THouseOwner{
|
||||
int HouseID;
|
||||
int OwnerID;
|
||||
char OwnerName[30];
|
||||
int PaidUntil;
|
||||
};
|
||||
|
||||
struct TWorldConfig{
|
||||
int Type;
|
||||
int RebootTime;
|
||||
int Address;
|
||||
int Port;
|
||||
int MaxPlayers;
|
||||
int PremiumPlayerBuffer;
|
||||
int MaxNewbies;
|
||||
int PremiumNewbieBuffer;
|
||||
};
|
||||
|
||||
bool LoadHouseOwners(const char *WorldName, DynamicArray<THouseOwner> *HouseOwners);
|
||||
bool LoadWorldConfig(const char *WorldName, TWorldConfig *WorldConfig);
|
||||
|
||||
bool InitDatabase(void);
|
||||
void ExitDatabase(void);
|
||||
|
||||
#endif //TIBIA_QUERYMANAGER_HH_
|
||||
262858
src/sqlite3.c
Normal file
262858
src/sqlite3.c
Normal file
File diff suppressed because it is too large
Load Diff
13775
src/sqlite3.h
Normal file
13775
src/sqlite3.h
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user