Compare commits
10 Commits
601ce32ee8
...
edea08d11c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edea08d11c | ||
|
|
6052f24524 | ||
|
|
8c2a8461db | ||
|
|
093e1563df | ||
|
|
d36b26307d | ||
|
|
6aa13394a1 | ||
|
|
27a0df43b3 | ||
|
|
0ff6217227 | ||
|
|
eba55f8361 | ||
|
|
ebf536a791 |
8
Makefile
8
Makefile
@ -26,11 +26,11 @@ ifeq ($(DATABASE), sqlite)
|
|||||||
CFLAGS += -DDATABASE_SQLITE=1
|
CFLAGS += -DDATABASE_SQLITE=1
|
||||||
else ifeq ($(DATABASE), postgres)
|
else ifeq ($(DATABASE), postgres)
|
||||||
DATABASEOBJ = $(BUILDDIR)/database_postgres.obj
|
DATABASEOBJ = $(BUILDDIR)/database_postgres.obj
|
||||||
CFLAGS += -DDATABASE_POSTGRESQL=1
|
CFLAGS += -DDATABASE_POSTGRESQL=1 -I/usr/include/postgresql
|
||||||
LFLAGS += -lpq
|
LFLAGS += -lpq
|
||||||
else ifeq ($(DATABASE), mariadb)
|
else ifeq ($(DATABASE), mariadb)
|
||||||
DATABASEOBJ = $(BUILDDIR)/database_mysql.obj
|
DATABASEOBJ = $(BUILDDIR)/database_mariadb.obj
|
||||||
CFLAGS += -DDATABASE_MYSQL=1 -DDATABASE_MARIADB=1
|
CFLAGS += -DDATABASE_MARIADB=1 -I/usr/include/mariadb
|
||||||
LFLAGS += -lmariadb
|
LFLAGS += -lmariadb
|
||||||
else
|
else
|
||||||
$(error Unsupported DATABASE: `$(DATABASE)`. Valid options are `sqlite`, `postgres`, or `mariadb`)
|
$(error Unsupported DATABASE: `$(DATABASE)`. Valid options are `sqlite`, `postgres`, or `mariadb`)
|
||||||
@ -68,7 +68,7 @@ $(BUILDDIR)/database_postgres.obj: $(SRCDIR)/database_postgres.cc $(SRCDIR)/quer
|
|||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
||||||
|
|
||||||
$(BUILDDIR)/database_mysql.obj: $(SRCDIR)/database_mysql.cc $(SRCDIR)/querymanager.hh
|
$(BUILDDIR)/database_mariadb.obj: $(SRCDIR)/database_mariadb.cc $(SRCDIR)/querymanager.hh
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ make clean # remove `build` directory
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Running (SQLite)
|
## Running (SQLite)
|
||||||
The query manager becomes the database, automatically initializing and maintaining the schema, based on the files in `sqlite/` (see `sqlite/README.txt`). The default schema file won't automatically insert any initial data (see `sqlite/init.sql`), although you could put some insertions at the end to avoid having to manually run something like `sqlite/init.sql`. There are a few configuration options, and in particular `SQLite.*` options that can be adjusted in `config.cfg` but the defaults should work for most use cases.
|
The query manager becomes the database, automatically initializing and maintaining the schema, based on the files in `sqlite/` (see `sqlite/README.txt`). The default schema file won't automatically insert any initial data but that can be changed by using a patch (again, see `sqlite/README.txt`). There are a few configuration options, and in particular `SQLite.*` options that can be adjusted in `config.cfg` but the defaults should work for most use cases.
|
||||||
|
|
||||||
## Running (PostgreSQL)
|
## Running (PostgreSQL)
|
||||||
The query manager becomes a relay to the actual database. And with PostgreSQL being a distributed database system, it makes no sense to have individual clients managing the schema, since there could be multiple, each with their own assumptions. For that reason there is a `SchemaInfo` table with a `VERSION` row that will be queried at startup and compared against `POSTGRESQL_SCHEMA_VERSION`, defined in `src/database_postgres.cc`, to make sure there is an agreement on the schema version. It is hardcoded because schema changes will usually result in query changes.
|
The query manager becomes a relay to the actual database. And with PostgreSQL being a distributed database system, it makes no sense to have individual clients managing the schema, since there could be multiple, each with their own assumptions. For that reason there is a `SchemaInfo` table with a `VERSION` row that will be queried at startup and compared against `POSTGRESQL_SCHEMA_VERSION`, defined in `src/database_postgres.cc`, to make sure there is an agreement on the schema version. It is hardcoded because schema changes will usually result in query changes.
|
||||||
|
|||||||
@ -11,8 +11,8 @@ SQLite.MaxCachedStatements = 100
|
|||||||
# Empty values are ignored, meaning their defaults will be used instead.
|
# Empty values are ignored, meaning their defaults will be used instead.
|
||||||
# For more information see:
|
# For more information see:
|
||||||
# https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
|
# https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
|
||||||
PostgreSQL.Host = "localhost"
|
PostgreSQL.Host = ""
|
||||||
PostgreSQL.Port = "5432"
|
PostgreSQL.Port = ""
|
||||||
PostgreSQL.DBName = "tibia"
|
PostgreSQL.DBName = "tibia"
|
||||||
PostgreSQL.User = "tibia"
|
PostgreSQL.User = "tibia"
|
||||||
PostgreSQL.Password = ""
|
PostgreSQL.Password = ""
|
||||||
@ -22,14 +22,14 @@ PostgreSQL.SSLMode = ""
|
|||||||
PostgreSQL.SSLRootCert = ""
|
PostgreSQL.SSLRootCert = ""
|
||||||
PostgreSQL.MaxCachedStatements = 100
|
PostgreSQL.MaxCachedStatements = 100
|
||||||
|
|
||||||
# MySQL/MariaDB Config
|
# MariaDB Config
|
||||||
MySQL.Host = "localhost"
|
MariaDB.Host = "localhost"
|
||||||
MySQL.Port = "3306"
|
MariaDB.Port = "3306"
|
||||||
MySQL.DBName = "tibia"
|
MariaDB.DBName = "tibia"
|
||||||
MySQL.User = "tibia"
|
MariaDB.User = "tibia"
|
||||||
MySQL.Password = ""
|
MariaDB.Password = ""
|
||||||
MySQL.UnixSocket = ""
|
MariaDB.UnixSocket = ""
|
||||||
MySQL.MaxCachedStatements = 100
|
MariaDB.MaxCachedStatements = 100
|
||||||
|
|
||||||
# Connection Config
|
# Connection Config
|
||||||
QueryManagerPort = 7173
|
QueryManagerPort = 7173
|
||||||
|
|||||||
146
postgres/README.txt
Normal file
146
postgres/README.txt
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
WARNING: This is not meant to be a complete guide on PostgreSQL, but rather a
|
||||||
|
"first steps" kind of guide. It'll only cover things on the surface level. For
|
||||||
|
a deep dive into how the database operates, how to properly configure it, and
|
||||||
|
ultimately properly administrate it, you MUST refer to the PostgreSQL manual
|
||||||
|
for your version. The most current version of the manual will describe the most
|
||||||
|
recent features, but not all features are present in all versions.
|
||||||
|
If anything, you should absolutely consult the section that regards server
|
||||||
|
administration "III. Server Administration".
|
||||||
|
|
||||||
|
MANUAL https://www.postgresql.org/docs/current/index.html
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
PostgreSQL is available in most Linux distributions as a package which is
|
||||||
|
the preferred way to get it installed. Some will automatically setup a service,
|
||||||
|
create service users, initialize the database cluster, etc... If not, you might
|
||||||
|
need to do one or more steps manually. If you're having trouble, most systems
|
||||||
|
will have specific instructions on how to set everything up. Just as an example
|
||||||
|
here are a few links for common systems:
|
||||||
|
DEBIAN https://www.postgresql.org/download/linux/debian/
|
||||||
|
REDHAT https://www.postgresql.org/download/linux/redhat/
|
||||||
|
SUSE https://www.postgresql.org/download/linux/suse/
|
||||||
|
UBUNTU https://www.postgresql.org/download/linux/ubuntu/
|
||||||
|
ARCH https://wiki.archlinux.org/title/PostgreSQL
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
By default, configuration files will be in the `data` directory which can
|
||||||
|
change locations depending on how the server was installed but is usually in
|
||||||
|
`/var/lib/postgres/data`.
|
||||||
|
All files in the `data` directory are OWNED by the *postgres* SYSTEM user,
|
||||||
|
meaning you'll only be able to modify them if you're logged in as *postgres*,
|
||||||
|
by using *sudo* privileges, or both with `sudo su postgres`.
|
||||||
|
The bulk of the configuration is inside `postgresql.conf` which has multiple
|
||||||
|
options, but of particular interest are the "CONNECTIONS AND AUTHENTICATION"
|
||||||
|
options. I won't go over specifics here but if you're planning on accepting
|
||||||
|
remote connections, you MUST properly configure SSL communication.
|
||||||
|
Access to the database is controlled with `pg_hba.conf`. This is different
|
||||||
|
from MySQL where you'd specify users as 'user'@'host' with SQL to restrict
|
||||||
|
them to certain hosts. Instead you need to specify how certain users/roles
|
||||||
|
may connect to the database in this file. Properly configuring it is probably
|
||||||
|
the most important step in securing the database, aside from configuring SSL
|
||||||
|
communication.
|
||||||
|
The last file is `pg_ident.conf` which declares mappings from system users
|
||||||
|
to database users. These mappings alone don't do anything. They must be
|
||||||
|
explicitly referenced as `map=MAPNAME` in `pg_hba.conf` for supported
|
||||||
|
authentication methods.
|
||||||
|
|
||||||
|
Here is an example of a `pg_hba.conf` + `pg_ident.conf` local access config.
|
||||||
|
It'll allow *systemuser* to connect as *postgres* to any database using the
|
||||||
|
*peer* method which checks the system user name. It'll also allow the *tibia*
|
||||||
|
user to connect to the *tibia* database using the *scram-sha-256* password
|
||||||
|
authentication scheme. Local connections will use UNIX-domain sockets and for
|
||||||
|
that matter you'd leave `PostgreSQL.Host` empty.
|
||||||
|
|
||||||
|
```
|
||||||
|
# pg_hba.conf
|
||||||
|
# TYPE DATABASE USER ADDRESS METHOD
|
||||||
|
local all postgres peer map=super
|
||||||
|
local tibia tibia scram-sha-256
|
||||||
|
|
||||||
|
# pg_ident.conf
|
||||||
|
# MAPNAME SYSTEM-USERNAME PG-USERNAME
|
||||||
|
super systemuser postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
MANUAL https://www.postgresql.org/docs/current/runtime-config.html
|
||||||
|
MANUAL https://www.postgresql.org/docs/current/client-authentication.html
|
||||||
|
|
||||||
|
Database Setup
|
||||||
|
--------------
|
||||||
|
It is highly advised to not use a SUPERUSER when connecting to the database
|
||||||
|
from the query manager, or any other service for that matter. This warrants the
|
||||||
|
creation of a secondary user that has access, but not administrative privileges.
|
||||||
|
I figured it would be simpler to have a sequence of *PSQL* commands with their
|
||||||
|
descriptions. Having a database minimaly ready for the query manager should be
|
||||||
|
a matter of following this sequence.
|
||||||
|
|
||||||
|
Unless a database is specified, *PSQL* will connect to one with the same name
|
||||||
|
as the specified user. If the user is not explicitly specified, the system user
|
||||||
|
name will be used. Running `psql -U postgres` will connect to *postgres* as the
|
||||||
|
user *postgres*. Note that you can't connect without a database, so you'd connect
|
||||||
|
to *postgres* in order to create new databases.
|
||||||
|
|
||||||
|
1 - Create and connect to a new database. Note that the `OWNER = postgres` clause
|
||||||
|
is redundant here but it's just to show that having the database owned by the
|
||||||
|
super user is intended.
|
||||||
|
```
|
||||||
|
psql -U postgres -c "CREATE DATABASE tibia OWNER = postgres;"
|
||||||
|
psql -U postgres tibia
|
||||||
|
```
|
||||||
|
|
||||||
|
2 - Set default privileges. Newly created databases may have some default PUBLIC
|
||||||
|
privileges that we'll want to revoke to make sure the set of users that are able
|
||||||
|
to connect is tighly controlled. Then, for users that are able to connect, we
|
||||||
|
want to grant default access privileges on tables, while revoking the ability to
|
||||||
|
create or rename objects (tables, views, sequences, indexes). Note that a schema
|
||||||
|
in PostgreSQL is just a namespace for objects and new databases should have the
|
||||||
|
*public* schema created by default.
|
||||||
|
```
|
||||||
|
REVOKE ALL ON DATABASE tibia FROM PUBLIC;
|
||||||
|
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE
|
||||||
|
ON TABLES TO PUBLIC;
|
||||||
|
```
|
||||||
|
|
||||||
|
3 - Initialize schema. This is done by executing commands from `postgres/schema.sql`,
|
||||||
|
and optionally `postgres/initial-data.sql`. Note that since we set default privileges
|
||||||
|
before creating any tables, they should already have the approppriate privileges.
|
||||||
|
If done the other way around, we'd need to manually update table privileges.
|
||||||
|
```
|
||||||
|
\i postgres/schema.sql
|
||||||
|
\i postgres/initial-data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
4 - Create secondary user. This is straighforward. Create a user with *LOGIN*
|
||||||
|
privileges and a *PASSWORD*. Then grant *CONNECT* privileges to the database.
|
||||||
|
```
|
||||||
|
CREATE ROLE tibia WITH LOGIN PASSWORD '********';
|
||||||
|
GRANT CONNECT ON DATABASE tibia TO tibia;
|
||||||
|
```
|
||||||
|
|
||||||
|
This is just one way. There are probably other, more optimal setups, but
|
||||||
|
for a small testing bench, it will do. And don't take my word on anything.
|
||||||
|
You should always check the manual for a complete description on how things
|
||||||
|
work.
|
||||||
|
|
||||||
|
To wrap, here is a list of helpful commands available in *PSQL*. They'll
|
||||||
|
show up along with a lot of other commands when running `\?`.
|
||||||
|
```
|
||||||
|
\q # quit
|
||||||
|
\l # list databases (will show database privileges)
|
||||||
|
\du # list users (will show user privileges)
|
||||||
|
\dO # list collations
|
||||||
|
\dt # list tables
|
||||||
|
\dv # list views
|
||||||
|
\ds # list sequences
|
||||||
|
\di # list indexes
|
||||||
|
\d NAME # describe table/view/sequence/index
|
||||||
|
\dp # list privileges
|
||||||
|
\ddp # list default privileges
|
||||||
|
```
|
||||||
|
|
||||||
|
MANUAL https://www.postgresql.org/docs/current/sql-commands.html
|
||||||
|
|
||||||
@ -2,6 +2,8 @@
|
|||||||
-- the database in some partial state. Also, notice we don't use `IF NOT EXISTS`
|
-- the database in some partial state. Also, notice we don't use `IF NOT EXISTS`
|
||||||
-- because it could also leave the database in a bad state if for example, some
|
-- because it could also leave the database in a bad state if for example, some
|
||||||
-- old version of a table already existed.
|
-- old version of a table already existed.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
-- IMPORTANT(fusion): PosgreSQL doesn't have a defined `NOCASE` collation like
|
-- IMPORTANT(fusion): PosgreSQL doesn't have a defined `NOCASE` collation like
|
||||||
@ -78,9 +80,6 @@ CREATE TABLE Characters (
|
|||||||
AccountID INTEGER NOT NULL,
|
AccountID INTEGER NOT NULL,
|
||||||
Name TEXT NOT NULL COLLATE NOCASE,
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
Sex SMALLINT NOT NULL,
|
Sex SMALLINT NOT NULL,
|
||||||
Guild TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Rank TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Title TEXT NOT NULL DEFAULT '',
|
|
||||||
Level SMALLINT NOT NULL DEFAULT 0,
|
Level SMALLINT NOT NULL DEFAULT 0,
|
||||||
Profession TEXT NOT NULL DEFAULT '',
|
Profession TEXT NOT NULL DEFAULT '',
|
||||||
Residence TEXT NOT NULL DEFAULT '',
|
Residence TEXT NOT NULL DEFAULT '',
|
||||||
@ -93,7 +92,6 @@ CREATE TABLE Characters (
|
|||||||
);
|
);
|
||||||
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
||||||
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
||||||
CREATE INDEX CharactersGuildIndex ON Characters(Guild, Rank);
|
|
||||||
|
|
||||||
-- NOTE(fusion): It seems `RIGHT` is a reserved keyword and trying to use
|
-- NOTE(fusion): It seems `RIGHT` is a reserved keyword and trying to use
|
||||||
-- it as a column name will generate an error.
|
-- it as a column name will generate an error.
|
||||||
@ -111,8 +109,9 @@ CREATE TABLE CharacterDeaths (
|
|||||||
Unjustified BOOLEAN NOT NULL,
|
Unjustified BOOLEAN NOT NULL,
|
||||||
Timestamp TIMESTAMPTZ NOT NULL
|
Timestamp TIMESTAMPTZ NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX CharacterDeathsCharacterIndex ON CharacterDeaths(CharacterID, Level);
|
CREATE INDEX CharacterDeathsCharacterIndex ON CharacterDeaths(CharacterID, Timestamp);
|
||||||
CREATE INDEX CharacterDeathsOffenderIndex ON CharacterDeaths(OffenderID, Unjustified);
|
CREATE INDEX CharacterDeathsOffenderIndex ON CharacterDeaths(OffenderID, Timestamp);
|
||||||
|
CREATE INDEX CharacterDeathsTimeIndex ON CharacterDeaths(Timestamp);
|
||||||
|
|
||||||
CREATE TABLE Buddies (
|
CREATE TABLE Buddies (
|
||||||
WorldID INTEGER NOT NULL,
|
WorldID INTEGER NOT NULL,
|
||||||
@ -136,6 +135,46 @@ CREATE TABLE LoginAttempts (
|
|||||||
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
||||||
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
||||||
|
|
||||||
|
-- Guild Tables
|
||||||
|
--==============================================================================
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
-- House Tables
|
-- House Tables
|
||||||
--==============================================================================
|
--==============================================================================
|
||||||
CREATE TABLE Houses (
|
CREATE TABLE Houses (
|
||||||
|
|||||||
54
postgres/z-002-migrate-v02-to-v03.sql
Normal file
54
postgres/z-002-migrate-v02-to-v03.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- NOTE(fusion): This file contains the migration script from v0.2 to v0.3. It's
|
||||||
|
-- put inside a transaction to avoid errors from leaving the database in some
|
||||||
|
-- partial state.
|
||||||
|
-- These changes are already present in the latest `schema.sql`, so trying to
|
||||||
|
-- apply them on a newly created database will result in errors.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX CharactersGuildIndex;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Guild;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Rank;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Title;
|
||||||
|
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank SMALLINT NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
20
postgres/z-003-character-deaths-indexes.sql
Normal file
20
postgres/z-003-character-deaths-indexes.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- NOTE(fusion): This file contains index adjustments for the `CharacterDeaths`
|
||||||
|
-- table. It is not strictly necessary as there are no modified tables but should
|
||||||
|
-- help with queries such as the most recent deaths/kills from a specific character
|
||||||
|
-- or overall.
|
||||||
|
-- It's inside a transaction to avoid errors from leaving the database in some
|
||||||
|
-- partial state. The changes are already present in the latest `schema.sql`, so
|
||||||
|
-- trying to apply them on a newly created database will result in errors.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX CharacterDeathsCharacterIndex;
|
||||||
|
DROP INDEX CharacterDeathsOffenderIndex;
|
||||||
|
|
||||||
|
CREATE INDEX CharacterDeathsCharacterIndex ON CharacterDeaths(CharacterID, Timestamp);
|
||||||
|
CREATE INDEX CharacterDeathsOffenderIndex ON CharacterDeaths(OffenderID, Timestamp);
|
||||||
|
CREATE INDEX CharacterDeathsTimeIndex ON CharacterDeaths(Timestamp);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
-- NOTE(fusion): This file contains sample initial data. It's wrapped into a
|
-- NOTE(fusion): This file contains sample initial data. It's put inside a
|
||||||
-- transaction to avoid errors from leaving the database in some partial state.
|
-- transaction to avoid errors from leaving the database in some partial state.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
-- 111111/tibia
|
-- 111111/tibia
|
||||||
@ -7,5 +7,6 @@ files will be executed exactly ONCE. If multiple patches are pending at startup,
|
|||||||
they're executed in alphabetical order.
|
they're executed in alphabetical order.
|
||||||
As for statement restrictions, the only thing prohibited is the presence of
|
As for statement restrictions, the only thing prohibited is the presence of
|
||||||
transaction statements "BEGIN", "ROLLBACK", and "COMMIT". This is because all
|
transaction statements "BEGIN", "ROLLBACK", and "COMMIT". This is because all
|
||||||
patches will be bundled into the same transaction, to ensure atomicity.
|
patches will be bundled into the same transaction, to ensure atomicity, and
|
||||||
|
there can't be nested transactions.
|
||||||
|
|
||||||
|
|||||||
2
sqlite/patches/.gitignore
vendored
Normal file
2
sqlite/patches/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@ -57,9 +57,6 @@ CREATE TABLE Characters (
|
|||||||
AccountID INTEGER NOT NULL,
|
AccountID INTEGER NOT NULL,
|
||||||
Name TEXT NOT NULL COLLATE NOCASE,
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
Sex INTEGER NOT NULL,
|
Sex INTEGER NOT NULL,
|
||||||
Guild TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Rank TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
|
||||||
Title TEXT NOT NULL DEFAULT '',
|
|
||||||
Level INTEGER NOT NULL DEFAULT 0,
|
Level INTEGER NOT NULL DEFAULT 0,
|
||||||
Profession TEXT NOT NULL DEFAULT '',
|
Profession TEXT NOT NULL DEFAULT '',
|
||||||
Residence TEXT NOT NULL DEFAULT '',
|
Residence TEXT NOT NULL DEFAULT '',
|
||||||
@ -72,7 +69,6 @@ CREATE TABLE Characters (
|
|||||||
);
|
);
|
||||||
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
CREATE INDEX CharactersWorldIndex ON Characters(WorldID, IsOnline);
|
||||||
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
CREATE INDEX CharactersAccountIndex ON Characters(AccountID, IsOnline);
|
||||||
CREATE INDEX CharactersGuildIndex ON Characters(Guild, Rank);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
-- TODO(fusion): Have group rights instead of adding individual rights to characters?
|
-- TODO(fusion): Have group rights instead of adding individual rights to characters?
|
||||||
@ -98,8 +94,9 @@ CREATE TABLE CharacterDeaths (
|
|||||||
Unjustified INTEGER NOT NULL,
|
Unjustified INTEGER NOT NULL,
|
||||||
Timestamp INTEGER NOT NULL
|
Timestamp INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX CharacterDeathsCharacterIndex ON CharacterDeaths(CharacterID, Level);
|
CREATE INDEX CharacterDeathsCharacterIndex ON CharacterDeaths(CharacterID, Timestamp);
|
||||||
CREATE INDEX CharacterDeathsOffenderIndex ON CharacterDeaths(OffenderID, Unjustified);
|
CREATE INDEX CharacterDeathsOffenderIndex ON CharacterDeaths(OffenderID, Timestamp);
|
||||||
|
CREATE INDEX CharacterDeathsTimeIndex ON CharacterDeaths(Timestamp);
|
||||||
|
|
||||||
CREATE TABLE Buddies (
|
CREATE TABLE Buddies (
|
||||||
WorldID INTEGER NOT NULL,
|
WorldID INTEGER NOT NULL,
|
||||||
@ -123,6 +120,46 @@ CREATE TABLE LoginAttempts (
|
|||||||
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
CREATE INDEX LoginAttemptsAccountIndex ON LoginAttempts(AccountID, Timestamp);
|
||||||
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
CREATE INDEX LoginAttemptsAddressIndex ON LoginAttempts(IPAddress, Timestamp);
|
||||||
|
|
||||||
|
-- Guild Tables
|
||||||
|
--==============================================================================
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
-- House Tables
|
-- House Tables
|
||||||
--==============================================================================
|
--==============================================================================
|
||||||
CREATE TABLE Houses (
|
CREATE TABLE Houses (
|
||||||
|
|||||||
@ -1,14 +1,30 @@
|
|||||||
-- NOTE(fusion): This file contains the migration script from v0.1 to v0.2. It
|
-- NOTE(fusion): This file contains the migration script from v0.1 to v0.2. It
|
||||||
-- can be placed into `patches/` to upgrade an existing database. Note that these
|
-- must be manually executed as `sqlite3 -bail -echo tibia.db < migration.sql`
|
||||||
-- changes are already present in the latest `schema.sql`, so trying to patch a
|
-- because the original schema didn't have a `Patches` table which is necessary
|
||||||
-- newly created database will probably result in errors. See `sqlite/README.txt`
|
-- with the new automatic patching system. Future migration scripts can be placed
|
||||||
-- for more details.
|
-- in `patches/` for automatic execution but not this one, unfortunately. For
|
||||||
|
-- more details see `sqlite/README.txt`.
|
||||||
|
-- These changes are already present in the latest `schema.sql`, so trying to
|
||||||
|
-- apply them to a newly created database will result in errors.
|
||||||
--==============================================================================
|
--==============================================================================
|
||||||
|
|
||||||
ALTER TABLE Worlds RENAME COLUMN OnlineRecord TO OnlinePeak;
|
BEGIN;
|
||||||
ALTER TABLE Worlds RENAME COLUMN OnlineRecordTimestamp TO OnlinePeakTimestamp;
|
|
||||||
ALTER TABLE CharacterRights RENAME COLUMN Right TO Name;
|
|
||||||
|
|
||||||
|
PRAGMA application_id = 0x54694442;
|
||||||
|
|
||||||
|
PRAGMA user_version = 1;
|
||||||
|
|
||||||
|
CREATE TABLE Patches (
|
||||||
|
FileName TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
Timestamp INTEGER NOT NULL,
|
||||||
|
UNIQUE (FileName)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE Worlds RENAME COLUMN OnlineRecord TO OnlinePeak;
|
||||||
|
ALTER TABLE Worlds RENAME COLUMN OnlineRecordTimestamp TO OnlinePeakTimestamp;
|
||||||
ALTER TABLE Worlds ADD COLUMN LastStartup INTEGER NOT NULL DEFAULT 0;
|
ALTER TABLE Worlds ADD COLUMN LastStartup INTEGER NOT NULL DEFAULT 0;
|
||||||
ALTER TABLE Worlds ADD COLUMN LastShutdown INTEGER NOT NULL DEFAULT 0;
|
ALTER TABLE Worlds ADD COLUMN LastShutdown INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE CharacterRights RENAME COLUMN Right TO Name;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
|||||||
50
sqlite/z-002-migrate-v02-to-v03.sql
Normal file
50
sqlite/z-002-migrate-v02-to-v03.sql
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
-- NOTE(fusion): This file contains the migration script from v0.2 to v0.3. It
|
||||||
|
-- can be executed automatically as a patch if placed at `sqlite/patches`. For
|
||||||
|
-- more details see `sqlite/README.txt`.
|
||||||
|
-- These changes are already present in the latest `schema.sql`, so trying to
|
||||||
|
-- apply them to a newly created database will result in errors.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
|
DROP INDEX CharactersGuildIndex;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Guild;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Rank;
|
||||||
|
ALTER TABLE Characters DROP COLUMN Title;
|
||||||
|
|
||||||
|
CREATE TABLE Guilds (
|
||||||
|
WorldID INTEGER NOT NULL,
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
LeaderID INTEGER NOT NULL,
|
||||||
|
Created INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID),
|
||||||
|
UNIQUE (Name),
|
||||||
|
UNIQUE (LeaderID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildRanks (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, Rank)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE GuildMembers (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
Rank INTEGER NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Joined INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildMembersGuildIndex ON GuildMembers(GuildID, Rank);
|
||||||
|
|
||||||
|
CREATE TABLE GuildInvites (
|
||||||
|
GuildID INTEGER NOT NULL,
|
||||||
|
CharacterID INTEGER NOT NULL,
|
||||||
|
RecruiterID INTEGER NOT NULL,
|
||||||
|
Timestamp INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (GuildID, CharacterID)
|
||||||
|
);
|
||||||
|
CREATE INDEX GuildInvitesCharacterIndex ON GuildInvites(CharacterID);
|
||||||
|
CREATE INDEX GuildInvitesRecruiterIndex ON GuildInvites(RecruiterID);
|
||||||
|
|
||||||
17
sqlite/z-003-character-deaths-indexes.sql
Normal file
17
sqlite/z-003-character-deaths-indexes.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-- NOTE(fusion): This file contains index adjustments for the `CharacterDeaths`
|
||||||
|
-- table. It is not strictly necessary as there are no modified tables but should
|
||||||
|
-- help with queries such as the most recent deaths/kills from a specific character
|
||||||
|
-- or overall.
|
||||||
|
-- It can be executed automatically as a patch if placed at `sqlite/patches`. For
|
||||||
|
-- more details see `sqlite/README.txt`. The changes are already present in the
|
||||||
|
-- latest `schema.sql`, so trying to apply them on a newly created database will
|
||||||
|
-- result in errors.
|
||||||
|
--==============================================================================
|
||||||
|
|
||||||
|
DROP INDEX CharacterDeathsCharacterIndex;
|
||||||
|
DROP INDEX CharacterDeathsOffenderIndex;
|
||||||
|
|
||||||
|
CREATE INDEX CharacterDeathsCharacterIndex ON CharacterDeaths(CharacterID, Timestamp);
|
||||||
|
CREATE INDEX CharacterDeathsOffenderIndex ON CharacterDeaths(OffenderID, Timestamp);
|
||||||
|
CREATE INDEX CharacterDeathsTimeIndex ON CharacterDeaths(Timestamp);
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
-- NOTE(fusion): This file contains sample initial data and will be executed
|
-- NOTE(fusion): This file contains sample initial data and can be executed
|
||||||
-- automatically as a patch by the query manager. See `sqlite/README.txt` for
|
-- automatically as a patch by the query manager if placed at `sqlite/patches`.
|
||||||
-- more details.
|
-- See `sqlite/README.txt` for more details.
|
||||||
--==============================================================================
|
--==============================================================================
|
||||||
|
|
||||||
INSERT INTO Worlds (WorldID, Name, Type, RebootTime, Host, Port, MaxPlayers,
|
INSERT INTO Worlds (WorldID, Name, Type, RebootTime, Host, Port, MaxPlayers,
|
||||||
@ -189,9 +189,9 @@ void CheckConnectionInput(TConnection *Connection, int Events){
|
|||||||
int ReadSize = Connection->RWSize;
|
int ReadSize = Connection->RWSize;
|
||||||
if(ReadSize == 0){
|
if(ReadSize == 0){
|
||||||
if(Connection->RWPosition < 2){
|
if(Connection->RWPosition < 2){
|
||||||
ReadSize = 2 - Connection->RWPosition;
|
ReadSize = 2;
|
||||||
}else{
|
}else{
|
||||||
ReadSize = 6 - Connection->RWPosition;
|
ReadSize = 6;
|
||||||
}
|
}
|
||||||
ASSERT(ReadSize > 0);
|
ASSERT(ReadSize > 0);
|
||||||
}
|
}
|
||||||
@ -392,7 +392,8 @@ void CheckConnectionQueryRequest(TConnection *Connection){
|
|||||||
SendQueryFailed(Connection);
|
SendQueryFailed(Connection);
|
||||||
}
|
}
|
||||||
}else if(Connection->ApplicationType == APPLICATION_TYPE_LOGIN){
|
}else if(Connection->ApplicationType == APPLICATION_TYPE_LOGIN){
|
||||||
if(QueryType == QUERY_LOGIN_ACCOUNT){
|
// TODO(fusion): Probably have a custom query for querying world status?
|
||||||
|
if(QueryType == QUERY_LOGIN_ACCOUNT || QueryType == QUERY_GET_WORLDS){
|
||||||
ProcessQuery(Connection);
|
ProcessQuery(Connection);
|
||||||
}else{
|
}else{
|
||||||
LOG_ERR("Invalid LOGIN query %d (%s) from %s",
|
LOG_ERR("Invalid LOGIN query %d (%s) from %s",
|
||||||
@ -599,7 +600,7 @@ void ProcessConnections(void){
|
|||||||
}
|
}
|
||||||
|
|
||||||
for(int i = 0; i < g_Config.MaxConnections; i += 1){
|
for(int i = 0; i < g_Config.MaxConnections; i += 1){
|
||||||
if(g_Connections[i].State == CONNECTION_FREE || g_Connections[i].Socket == -1){
|
if(g_Connections[i].State == CONNECTION_FREE){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/database_mariadb.cc
Normal file
10
src/database_mariadb.cc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#if DATABASE_MARIADB
|
||||||
|
#include "querymanager.hh"
|
||||||
|
|
||||||
|
// TODO(fusion): If we decide to implement MySQL, we should use MariaDB instead.
|
||||||
|
// It is a better alternative overall, and targeting a single database system
|
||||||
|
// should help us leverage its strongest features, which may not always be
|
||||||
|
// supported by both.
|
||||||
|
#error "MariaDB is not currently supported."
|
||||||
|
|
||||||
|
#endif //DATABASE_MARIADB
|
||||||
@ -1,6 +0,0 @@
|
|||||||
#if DATABASE_MYSQL
|
|
||||||
#include "querymanager.hh"
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
#endif //DATABASE_MYSQL
|
|
||||||
@ -285,10 +285,20 @@ static int ResultAffectedRows(PGresult *Result){
|
|||||||
return AffectedRows;
|
return AffectedRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANT(fusion): `GetResult*` helpers need to properly handle NULL values.
|
||||||
|
// In text format, they're represented as empty strings which could cause parsing
|
||||||
|
// errors. In binary format, they're represented as zero length blobs which could
|
||||||
|
// cause assertions to fire.
|
||||||
|
|
||||||
static bool GetResultBool(PGresult *Result, int Row, int Col){
|
static bool GetResultBool(PGresult *Result, int Row, int Col){
|
||||||
bool Value = false;
|
bool Value = false;
|
||||||
|
if(PQgetisnull(Result, Row, Col)){
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
|
||||||
int Format = PQfformat(Result, Col);
|
int Format = PQfformat(Result, Col);
|
||||||
Oid Type = PQftype(Result, Col);
|
Oid Type = PQftype(Result, Col);
|
||||||
|
ASSERT(Format == 0 || Format == 1);
|
||||||
if(Format == 0){ // TEXT FORMAT
|
if(Format == 0){ // TEXT FORMAT
|
||||||
if(!ParseBoolean(&Value, PQgetvalue(Result, Row, Col))){
|
if(!ParseBoolean(&Value, PQgetvalue(Result, Row, Col))){
|
||||||
LOG_ERR("Failed to properly parse column (%d) %s as BOOLEAN",
|
LOG_ERR("Failed to properly parse column (%d) %s as BOOLEAN",
|
||||||
@ -341,6 +351,10 @@ static bool GetResultBool(PGresult *Result, int Row, int Col){
|
|||||||
|
|
||||||
static int GetResultInt(PGresult *Result, int Row, int Col){
|
static int GetResultInt(PGresult *Result, int Row, int Col){
|
||||||
int Value = 0;
|
int Value = 0;
|
||||||
|
if(PQgetisnull(Result, Row, Col)){
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
|
||||||
int Format = PQfformat(Result, Col);
|
int Format = PQfformat(Result, Col);
|
||||||
Oid Type = PQftype(Result, Col);
|
Oid Type = PQftype(Result, Col);
|
||||||
ASSERT(Format == 0 || Format == 1);
|
ASSERT(Format == 0 || Format == 1);
|
||||||
@ -402,6 +416,10 @@ static int GetResultInt(PGresult *Result, int Row, int Col){
|
|||||||
|
|
||||||
static const char *GetResultText(PGresult *Result, int Row, int Col){
|
static const char *GetResultText(PGresult *Result, int Row, int Col){
|
||||||
const char *Text = "";
|
const char *Text = "";
|
||||||
|
if(PQgetisnull(Result, Row, Col)){
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
|
||||||
int Format = PQfformat(Result, Col);
|
int Format = PQfformat(Result, Col);
|
||||||
Oid Type = PQftype(Result, Col);
|
Oid Type = PQftype(Result, Col);
|
||||||
ASSERT(Format == 0 || Format == 1);
|
ASSERT(Format == 0 || Format == 1);
|
||||||
@ -433,6 +451,10 @@ static const char *GetResultText(PGresult *Result, int Row, int Col){
|
|||||||
|
|
||||||
static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int BufferSize){
|
static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int BufferSize){
|
||||||
int Size = -1;
|
int Size = -1;
|
||||||
|
if(PQgetisnull(Result, Row, Col)){
|
||||||
|
return Size;
|
||||||
|
}
|
||||||
|
|
||||||
int Format = PQfformat(Result, Col);
|
int Format = PQfformat(Result, Col);
|
||||||
ASSERT(Format == 0 || Format == 1);
|
ASSERT(Format == 0 || Format == 1);
|
||||||
if(Format == 0){ // TEXT FORMAT
|
if(Format == 0){ // TEXT FORMAT
|
||||||
@ -459,6 +481,10 @@ static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int
|
|||||||
// off to avoid compiler warnings.
|
// off to avoid compiler warnings.
|
||||||
static int GetResultIPAddress(PGresult *Result, int Row, int Col){
|
static int GetResultIPAddress(PGresult *Result, int Row, int Col){
|
||||||
int IPAddress = 0;
|
int IPAddress = 0;
|
||||||
|
if(PQgetisnull(Result, Row, Col)){
|
||||||
|
return IPAddress;
|
||||||
|
}
|
||||||
|
|
||||||
int Format = PQfformat(Result, Col);
|
int Format = PQfformat(Result, Col);
|
||||||
Oid Type = PQftype(Result, Col);
|
Oid Type = PQftype(Result, Col);
|
||||||
ASSERT(Format == 0 || Format == 1);
|
ASSERT(Format == 0 || Format == 1);
|
||||||
@ -573,6 +599,10 @@ static bool ParseTimestamp(int *Dest, const char *String){
|
|||||||
|
|
||||||
static int GetResultTimestamp(PGresult *Result, int Row, int Col){
|
static int GetResultTimestamp(PGresult *Result, int Row, int Col){
|
||||||
int Timestamp = 0;
|
int Timestamp = 0;
|
||||||
|
if(PQgetisnull(Result, Row, Col)){
|
||||||
|
return Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
int Format = PQfformat(Result, Col);
|
int Format = PQfformat(Result, Col);
|
||||||
Oid Type = PQftype(Result, Col);
|
Oid Type = PQftype(Result, Col);
|
||||||
ASSERT(Format == 0 || Format == 1);
|
ASSERT(Format == 0 || Format == 1);
|
||||||
@ -842,6 +872,10 @@ static bool ParseInterval(int *Dest, const char *String){
|
|||||||
|
|
||||||
static int GetResultInterval(PGresult *Result, int Row, int Col){
|
static int GetResultInterval(PGresult *Result, int Row, int Col){
|
||||||
int Interval = 0;
|
int Interval = 0;
|
||||||
|
if(PQgetisnull(Result, Row, Col)){
|
||||||
|
return Interval;
|
||||||
|
}
|
||||||
|
|
||||||
int Format = PQfformat(Result, Col);
|
int Format = PQfformat(Result, Col);
|
||||||
Oid Type = PQftype(Result, Col);
|
Oid Type = PQftype(Result, Col);
|
||||||
ASSERT(Format == 0 || Format == 1);
|
ASSERT(Format == 0 || Format == 1);
|
||||||
@ -1237,37 +1271,6 @@ TDatabase *DatabaseOpen(void){
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
{
|
|
||||||
// TODO(fusion): REMOVE. This is for testing query input/output, to make sure
|
|
||||||
// they're consitent across different formats (text/binary).
|
|
||||||
const char *Stmt = PrepareQuery(Database,
|
|
||||||
"SELECT $1::INTERVAL, $2::INTERVAL");
|
|
||||||
ASSERT(Stmt != NULL);
|
|
||||||
|
|
||||||
for(int i = 0; i <= 1; i += 1)
|
|
||||||
for(int j = 0; j <= 1; j += 1){
|
|
||||||
LOG("TEST (%d, %d)", i, j);
|
|
||||||
ParamBuffer Params;
|
|
||||||
ParamBegin(&Params, 2, i);
|
|
||||||
ParamInterval(&Params, 86400 + 3600);
|
|
||||||
ParamInterval(&Params, - 86400 * 4 + 7 * 3600);
|
|
||||||
PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
|
|
||||||
Params.Values, Params.Lengths, Params.Formats, j);
|
|
||||||
AutoResultClear ResultGuard(Result);
|
|
||||||
if(PQresultStatus(Result) == PGRES_TUPLES_OK){
|
|
||||||
LOG("0: %d", GetResultInterval(Result, 0, 0));
|
|
||||||
LOG("1: %d", GetResultInterval(Result, 0, 1));
|
|
||||||
}else{
|
|
||||||
LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseClose(Database);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return Database;
|
return Database;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1768,8 +1771,7 @@ bool GetCharacterID(TDatabase *Database, int WorldID, const char *CharacterName,
|
|||||||
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
const char *Stmt = PrepareQuery(Database,
|
const char *Stmt = PrepareQuery(Database,
|
||||||
"SELECT WorldID, CharacterID, AccountID, Name,"
|
"SELECT WorldID, CharacterID, AccountID, Name, Sex, Deleted"
|
||||||
" Sex, Guild, Rank, Title, Deleted"
|
|
||||||
" FROM Characters WHERE Name = $1::TEXT");
|
" FROM Characters WHERE Name = $1::TEXT");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
@ -1794,10 +1796,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
Character->AccountID = GetResultInt(Result, 0, 2);
|
Character->AccountID = GetResultInt(Result, 0, 2);
|
||||||
StringBufCopy(Character->Name, GetResultText(Result, 0, 3));
|
StringBufCopy(Character->Name, GetResultText(Result, 0, 3));
|
||||||
Character->Sex = GetResultInt(Result, 0, 4);
|
Character->Sex = GetResultInt(Result, 0, 4);
|
||||||
StringBufCopy(Character->Guild, GetResultText(Result, 0, 5));
|
Character->Deleted = GetResultBool(Result, 0, 5);
|
||||||
StringBufCopy(Character->Rank, GetResultText(Result, 0, 6));
|
|
||||||
StringBufCopy(Character->Title, GetResultText(Result, 0, 7));
|
|
||||||
Character->Deleted = GetResultBool(Result, 0, 8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1806,7 +1805,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
const char *Stmt = PrepareQuery(Database,
|
const char *Stmt = PrepareQuery(Database,
|
||||||
"SELECT C.Name, W.Name, C.Sex, C.Guild, C.Rank, C.Title, C.Level,"
|
"SELECT C.CharacterID, C.Name, W.Name, C.Sex, C.Level,"
|
||||||
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
||||||
" C.Deleted, GREATEST(A.PremiumEnd - CURRENT_TIMESTAMP, '0')"
|
" C.Deleted, GREATEST(A.PremiumEnd - CURRENT_TIMESTAMP, '0')"
|
||||||
" FROM Characters AS C"
|
" FROM Characters AS C"
|
||||||
@ -1814,7 +1813,7 @@ bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharac
|
|||||||
" LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID"
|
" LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID"
|
||||||
" LEFT JOIN CharacterRights AS R"
|
" LEFT JOIN CharacterRights AS R"
|
||||||
" ON R.CharacterID = C.CharacterID"
|
" ON R.CharacterID = C.CharacterID"
|
||||||
" AND R.Name = 'NO_STATISTICS'"
|
" AND R.Name = 'NO_STATISTICS'"
|
||||||
" WHERE C.Name = $1::TEXT AND R.Name IS NULL");
|
" WHERE C.Name = $1::TEXT AND R.Name IS NULL");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
@ -1834,19 +1833,17 @@ bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharac
|
|||||||
|
|
||||||
memset(Character, 0, sizeof(TCharacterProfile));
|
memset(Character, 0, sizeof(TCharacterProfile));
|
||||||
if(PQntuples(Result) > 0){
|
if(PQntuples(Result) > 0){
|
||||||
StringBufCopy(Character->Name, GetResultText(Result, 0, 0));
|
Character->CharacterID = GetResultInt(Result, 0, 0);
|
||||||
StringBufCopy(Character->World, GetResultText(Result, 0, 1));
|
StringBufCopy(Character->Name, GetResultText(Result, 0, 1));
|
||||||
Character->Sex = GetResultInt(Result, 0, 2);
|
StringBufCopy(Character->World, GetResultText(Result, 0, 2));
|
||||||
StringBufCopy(Character->Guild, GetResultText(Result, 0, 3));
|
Character->Sex = GetResultInt(Result, 0, 3);
|
||||||
StringBufCopy(Character->Rank, GetResultText(Result, 0, 4));
|
Character->Level = GetResultInt(Result, 0, 4);
|
||||||
StringBufCopy(Character->Title, GetResultText(Result, 0, 5));
|
StringBufCopy(Character->Profession, GetResultText(Result, 0, 5));
|
||||||
Character->Level = GetResultInt(Result, 0, 6);
|
StringBufCopy(Character->Residence, GetResultText(Result, 0, 6));
|
||||||
StringBufCopy(Character->Profession, GetResultText(Result, 0, 7));
|
Character->LastLogin = GetResultTimestamp(Result, 0, 7);
|
||||||
StringBufCopy(Character->Residence, GetResultText(Result, 0, 8));
|
Character->Online = GetResultBool(Result, 0, 8);
|
||||||
Character->LastLogin = GetResultTimestamp(Result, 0, 9);
|
Character->Deleted = GetResultBool(Result, 0, 9);
|
||||||
Character->Online = GetResultBool(Result, 0, 10);
|
Character->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 10));
|
||||||
Character->Deleted = GetResultBool(Result, 0, 11);
|
|
||||||
Character->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1912,8 +1909,8 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
ASSERT(Database != NULL && GuildLeader != NULL);
|
ASSERT(Database != NULL && GuildLeader != NULL);
|
||||||
// NOTE(fusion): Same as `DecrementIsOnline`.
|
// NOTE(fusion): Same as `DecrementIsOnline`.
|
||||||
const char *Stmt = PrepareQuery(Database,
|
const char *Stmt = PrepareQuery(Database,
|
||||||
"SELECT Guild, Rank FROM Characters"
|
"SELECT 1 FROM Guilds"
|
||||||
" WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER");
|
" WHERE WorldID = $1::INTEGER AND LeaderID = $2::INTEGER");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
return false;
|
return false;
|
||||||
@ -1931,16 +1928,8 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*GuildLeader = false;
|
*GuildLeader = (PQntuples(Result) > 0);
|
||||||
if(PQntuples(Result) > 0){
|
return true;
|
||||||
const char *Guild = GetResultText(Result, 0, 0);
|
|
||||||
const char *Rank = GetResultText(Result, 0, 1);
|
|
||||||
if(Guild != NULL && !StringEmpty(Guild) && Rank != NULL && StringEqCI(Rank, "Leader")){
|
|
||||||
*GuildLeader = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IncrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){
|
bool IncrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){
|
||||||
@ -2338,6 +2327,46 @@ bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int Tim
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guild Tables
|
||||||
|
//==============================================================================
|
||||||
|
bool GetCharacterGuildData(TDatabase *Database, int CharacterID, TCharacterGuildData *GuildData){
|
||||||
|
ASSERT(Database != NULL && GuildData != NULL);
|
||||||
|
const char *Stmt = PrepareQuery(Database,
|
||||||
|
"SELECT G.GuildID, R.Rank, G.Name, R.Name, M.Title"
|
||||||
|
" FROM Characters AS C"
|
||||||
|
" LEFT JOIN GuildMembers AS M ON M.CharacterID = C.CharacterID"
|
||||||
|
" LEFT JOIN Guilds AS G ON G.GuildID = M.GuildID"
|
||||||
|
" LEFT JOIN GuildRanks AS R"
|
||||||
|
" ON R.GuildID = M.GuildID AND R.Rank = M.Rank"
|
||||||
|
" WHERE C.CharacterID = $1::INTEGER");
|
||||||
|
if(Stmt == NULL){
|
||||||
|
LOG_ERR("Failed to prepare query");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParamBuffer Params = {};
|
||||||
|
ParamBegin(&Params, 1, 1);
|
||||||
|
ParamInt(&Params, CharacterID);
|
||||||
|
PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
|
||||||
|
Params.Values, Params.Lengths, Params.Formats, 1);
|
||||||
|
AutoResultClear ResultGuard(Result);
|
||||||
|
if(PQresultStatus(Result) != PGRES_TUPLES_OK){
|
||||||
|
LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(GuildData, 0, sizeof(TCharacterGuildData));
|
||||||
|
if(PQntuples(Result) > 0){
|
||||||
|
GuildData->GuildID = GetResultInt(Result, 0, 0);
|
||||||
|
GuildData->Rank = GetResultInt(Result, 0, 1);
|
||||||
|
StringBufCopy(GuildData->GuildName, GetResultText(Result, 0, 2));
|
||||||
|
StringBufCopy(GuildData->RankName, GetResultText(Result, 0, 3));
|
||||||
|
StringBufCopy(GuildData->Title, GetResultText(Result, 0, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// House Tables
|
// House Tables
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
||||||
@ -2412,7 +2441,7 @@ bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray<THouseT
|
|||||||
Transfer.HouseID = GetResultInt(Result, Row, 0);
|
Transfer.HouseID = GetResultInt(Result, Row, 0);
|
||||||
Transfer.NewOwnerID = GetResultInt(Result, Row, 1);
|
Transfer.NewOwnerID = GetResultInt(Result, Row, 1);
|
||||||
Transfer.Price = GetResultInt(Result, Row, 2);
|
Transfer.Price = GetResultInt(Result, Row, 2);
|
||||||
StringBufCopy(Transfer.NewOwnerName, GetResultText(Result, Row, 4));
|
StringBufCopy(Transfer.NewOwnerName, GetResultText(Result, Row, 3));
|
||||||
Transfers->Push(Transfer);
|
Transfers->Push(Transfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -412,9 +412,8 @@ static bool CheckDatabaseSchema(TDatabase *Database){
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT(fusion): After checking the application id, we want to apply
|
// IMPORTANT(fusion): We want to apply patches before checking user version,
|
||||||
// patches before checking user version, just in case some migration needs
|
// just in case some migration needs to take place.
|
||||||
// to take place.
|
|
||||||
if(!ApplyDatabasePatches(Database, "sqlite/patches")){
|
if(!ApplyDatabasePatches(Database, "sqlite/patches")){
|
||||||
LOG_ERR("Failed to apply database patches");
|
LOG_ERR("Failed to apply database patches");
|
||||||
return false;
|
return false;
|
||||||
@ -1023,8 +1022,7 @@ bool GetCharacterID(TDatabase *Database, int WorldID, const char *CharacterName,
|
|||||||
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
"SELECT WorldID, CharacterID, AccountID, Name,"
|
"SELECT WorldID, CharacterID, AccountID, Name, Sex, Deleted"
|
||||||
" Sex, Guild, Rank, Title, Deleted"
|
|
||||||
" FROM Characters WHERE Name = ?1");
|
" FROM Characters WHERE Name = ?1");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
@ -1050,10 +1048,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
Character->AccountID = sqlite3_column_int(Stmt, 2);
|
Character->AccountID = sqlite3_column_int(Stmt, 2);
|
||||||
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 3));
|
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 3));
|
||||||
Character->Sex = sqlite3_column_int(Stmt, 4);
|
Character->Sex = sqlite3_column_int(Stmt, 4);
|
||||||
StringBufCopy(Character->Guild, (const char*)sqlite3_column_text(Stmt, 5));
|
Character->Deleted = (sqlite3_column_int(Stmt, 5) != 0);
|
||||||
StringBufCopy(Character->Rank, (const char*)sqlite3_column_text(Stmt, 6));
|
|
||||||
StringBufCopy(Character->Title, (const char*)sqlite3_column_text(Stmt, 7));
|
|
||||||
Character->Deleted = (sqlite3_column_int(Stmt, 8) != 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1062,7 +1057,7 @@ bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TChar
|
|||||||
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){
|
||||||
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
ASSERT(Database != NULL && CharacterName != NULL && Character != NULL);
|
||||||
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
"SELECT C.Name, W.Name, C.Sex, C.Guild, C.Rank, C.Title, C.Level,"
|
"SELECT C.CharacterID, C.Name, W.Name, C.Sex, C.Level,"
|
||||||
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
" C.Profession, C.Residence, C.LastLoginTime, C.IsOnline,"
|
||||||
" C.Deleted, MAX(A.PremiumEnd - UNIXEPOCH(), 0)"
|
" C.Deleted, MAX(A.PremiumEnd - UNIXEPOCH(), 0)"
|
||||||
" FROM Characters AS C"
|
" FROM Characters AS C"
|
||||||
@ -1070,7 +1065,7 @@ bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharac
|
|||||||
" LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID"
|
" LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID"
|
||||||
" LEFT JOIN CharacterRights AS R"
|
" LEFT JOIN CharacterRights AS R"
|
||||||
" ON R.CharacterID = C.CharacterID"
|
" ON R.CharacterID = C.CharacterID"
|
||||||
" AND R.Name = 'NO_STATISTICS'"
|
" AND R.Name = 'NO_STATISTICS'"
|
||||||
" WHERE C.Name = ?1 AND R.Name IS NULL");
|
" WHERE C.Name = ?1 AND R.Name IS NULL");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
@ -1091,19 +1086,17 @@ bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharac
|
|||||||
|
|
||||||
memset(Character, 0, sizeof(TCharacterProfile));
|
memset(Character, 0, sizeof(TCharacterProfile));
|
||||||
if(ErrorCode == SQLITE_ROW){
|
if(ErrorCode == SQLITE_ROW){
|
||||||
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 0));
|
Character->CharacterID = sqlite3_column_int(Stmt, 0);
|
||||||
StringBufCopy(Character->World, (const char*)sqlite3_column_text(Stmt, 1));
|
StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 1));
|
||||||
Character->Sex = sqlite3_column_int(Stmt, 2);
|
StringBufCopy(Character->World, (const char*)sqlite3_column_text(Stmt, 2));
|
||||||
StringBufCopy(Character->Guild, (const char*)sqlite3_column_text(Stmt, 3));
|
Character->Sex = sqlite3_column_int(Stmt, 3);
|
||||||
StringBufCopy(Character->Rank, (const char*)sqlite3_column_text(Stmt, 4));
|
Character->Level = sqlite3_column_int(Stmt, 4);
|
||||||
StringBufCopy(Character->Title, (const char*)sqlite3_column_text(Stmt, 5));
|
StringBufCopy(Character->Profession, (const char*)sqlite3_column_text(Stmt, 5));
|
||||||
Character->Level = sqlite3_column_int(Stmt, 6);
|
StringBufCopy(Character->Residence, (const char*)sqlite3_column_text(Stmt, 6));
|
||||||
StringBufCopy(Character->Profession, (const char*)sqlite3_column_text(Stmt, 7));
|
Character->LastLogin = sqlite3_column_int(Stmt, 7);
|
||||||
StringBufCopy(Character->Residence, (const char*)sqlite3_column_text(Stmt, 8));
|
Character->Online = (sqlite3_column_int(Stmt, 8) != 0);
|
||||||
Character->LastLogin = sqlite3_column_int(Stmt, 9);
|
Character->Deleted = (sqlite3_column_int(Stmt, 9) != 0);
|
||||||
Character->Online = (sqlite3_column_int(Stmt, 10) != 0);
|
Character->PremiumDays = RoundSecondsToDays(sqlite3_column_int(Stmt, 10));
|
||||||
Character->Deleted = (sqlite3_column_int(Stmt, 11) != 0);
|
|
||||||
Character->PremiumDays = RoundSecondsToDays(sqlite3_column_int(Stmt, 12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -1169,8 +1162,8 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
ASSERT(Database != NULL && GuildLeader != NULL);
|
ASSERT(Database != NULL && GuildLeader != NULL);
|
||||||
// NOTE(fusion): Same as `DecrementIsOnline`.
|
// NOTE(fusion): Same as `DecrementIsOnline`.
|
||||||
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
"SELECT Guild, Rank FROM Characters"
|
"SELECT 1 FROM Guilds"
|
||||||
" WHERE WorldID = ?1 AND CharacterID = ?2");
|
" WHERE WorldID = ?1 AND LeaderID = ?2");
|
||||||
if(Stmt == NULL){
|
if(Stmt == NULL){
|
||||||
LOG_ERR("Failed to prepare query");
|
LOG_ERR("Failed to prepare query");
|
||||||
return false;
|
return false;
|
||||||
@ -1189,15 +1182,7 @@ bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, boo
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*GuildLeader = false;
|
*GuildLeader = (ErrorCode == SQLITE_ROW);
|
||||||
if(ErrorCode == SQLITE_ROW){
|
|
||||||
const char *Guild = (const char*)sqlite3_column_text(Stmt, 0);
|
|
||||||
const char *Rank = (const char*)sqlite3_column_text(Stmt, 1);
|
|
||||||
if(Guild != NULL && !StringEmpty(Guild) && Rank != NULL && StringEqCI(Rank, "Leader")){
|
|
||||||
*GuildLeader = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1593,6 +1578,47 @@ bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int Tim
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guild Tables
|
||||||
|
//==============================================================================
|
||||||
|
bool GetCharacterGuildData(TDatabase *Database, int CharacterID, TCharacterGuildData *GuildData){
|
||||||
|
ASSERT(Database != NULL && GuildData != NULL);
|
||||||
|
sqlite3_stmt *Stmt = PrepareQuery(Database,
|
||||||
|
"SELECT G.GuildID, R.Rank, G.Name, R.Name, M.Title"
|
||||||
|
" FROM Characters AS C"
|
||||||
|
" LEFT JOIN GuildMembers AS M ON M.CharacterID = C.CharacterID"
|
||||||
|
" LEFT JOIN Guilds AS G ON G.GuildID = M.GuildID"
|
||||||
|
" LEFT JOIN GuildRanks AS R"
|
||||||
|
" ON R.GuildID = M.GuildID AND R.Rank = M.Rank"
|
||||||
|
" WHERE C.CharacterID = ?1");
|
||||||
|
if(Stmt == NULL){
|
||||||
|
LOG_ERR("Failed to prepare query");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoStmtReset StmtReset(Stmt);
|
||||||
|
if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){
|
||||||
|
LOG_ERR("Failed to bind CharacterID: %s", sqlite3_errmsg(Database->Handle));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ErrorCode = sqlite3_step(Stmt);
|
||||||
|
if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){
|
||||||
|
LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(GuildData, 0, sizeof(TCharacterGuildData));
|
||||||
|
if(ErrorCode == SQLITE_ROW){
|
||||||
|
GuildData->GuildID = sqlite3_column_int(Stmt, 0);
|
||||||
|
GuildData->Rank = sqlite3_column_int(Stmt, 1);
|
||||||
|
StringBufCopy(GuildData->GuildName, (const char*)sqlite3_column_text(Stmt, 2));
|
||||||
|
StringBufCopy(GuildData->RankName, (const char*)sqlite3_column_text(Stmt, 3));
|
||||||
|
StringBufCopy(GuildData->Title, (const char*)sqlite3_column_text(Stmt, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// House Tables
|
// House Tables
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions){
|
||||||
@ -1660,7 +1686,7 @@ bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray<THouseT
|
|||||||
Transfer.HouseID = sqlite3_column_int(Stmt, 0);
|
Transfer.HouseID = sqlite3_column_int(Stmt, 0);
|
||||||
Transfer.NewOwnerID = sqlite3_column_int(Stmt, 1);
|
Transfer.NewOwnerID = sqlite3_column_int(Stmt, 1);
|
||||||
Transfer.Price = sqlite3_column_int(Stmt, 2);
|
Transfer.Price = sqlite3_column_int(Stmt, 2);
|
||||||
StringBufCopy(Transfer.NewOwnerName, (const char*)sqlite3_column_text(Stmt, 4));
|
StringBufCopy(Transfer.NewOwnerName, (const char*)sqlite3_column_text(Stmt, 3));
|
||||||
Transfers->Push(Transfer);
|
Transfers->Push(Transfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
256
src/query.cc
256
src/query.cc
@ -515,19 +515,35 @@ void ProcessInternalResolveWorld(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
static void CheckAccountPasswordTx(TDatabase *Database, TQuery *Query,
|
static void CheckAccountPasswordTx(TDatabase *Database, TQuery *Query,
|
||||||
int AccountID, const char *Password, int IPAddress){
|
int AccountID, const char *Password, int IPAddress){
|
||||||
|
enum{
|
||||||
|
E_ACCOUNT_NOT_FOUND = 1,
|
||||||
|
E_PASSWORD_MISMATCH = 2,
|
||||||
|
E_ACCOUNT_DISABLED = 3,
|
||||||
|
E_IPADDRESS_BLOCKED = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// IMPORTANT(fusion): Check whether credentials are empty, in which case
|
||||||
|
// we can skip accessing the database altogether, but also prevent errors
|
||||||
|
// such as "ip-address blocked" or "account disabled" for account id ZERO.
|
||||||
|
QUERY_ERROR_IF(AccountID == 0, E_ACCOUNT_NOT_FOUND);
|
||||||
|
QUERY_ERROR_IF(StringEmpty(Password), E_PASSWORD_MISMATCH);
|
||||||
|
|
||||||
TransactionScope Tx("CheckAccountPassword");
|
TransactionScope Tx("CheckAccountPassword");
|
||||||
QUERY_STOP_IF(!Tx.Begin(Database));
|
QUERY_STOP_IF(!Tx.Begin(Database));
|
||||||
|
|
||||||
|
// IMPORTANT(fusion): Disallow blocked IP addresses and accounts before
|
||||||
|
// checking credentials to prevent error messages from being used as an
|
||||||
|
// oracle for brute force attacks.
|
||||||
|
int FailedLoginAttempts;
|
||||||
|
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
|
||||||
|
QUERY_ERROR_IF(FailedLoginAttempts >= 20, E_IPADDRESS_BLOCKED);
|
||||||
|
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts));
|
||||||
|
QUERY_ERROR_IF(FailedLoginAttempts >= 10, E_ACCOUNT_DISABLED);
|
||||||
|
|
||||||
TAccount Account;
|
TAccount Account;
|
||||||
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
|
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
|
||||||
QUERY_ERROR_IF(Account.AccountID == 0, 1);
|
QUERY_ERROR_IF(Account.AccountID == 0, E_ACCOUNT_NOT_FOUND);
|
||||||
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 2);
|
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), E_PASSWORD_MISMATCH);
|
||||||
|
|
||||||
int FailedLoginAttempts;
|
|
||||||
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, Account.AccountID, 5 * 60, &FailedLoginAttempts));
|
|
||||||
QUERY_ERROR_IF(FailedLoginAttempts > 10, 3);
|
|
||||||
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
|
|
||||||
QUERY_ERROR_IF(FailedLoginAttempts > 20, 4);
|
|
||||||
|
|
||||||
QUERY_STOP_IF(!Tx.Commit());
|
QUERY_STOP_IF(!Tx.Commit());
|
||||||
QueryOk(Query);
|
QueryOk(Query);
|
||||||
@ -555,25 +571,43 @@ void ProcessCheckAccountPassword(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
static void LoginAccountTx(TDatabase *Database, TQuery *Query,
|
static void LoginAccountTx(TDatabase *Database, TQuery *Query,
|
||||||
int AccountID, const char *Password, int IPAddress){
|
int AccountID, const char *Password, int IPAddress){
|
||||||
|
enum{
|
||||||
|
E_ACCOUNT_NOT_FOUND = 1,
|
||||||
|
E_PASSWORD_MISMATCH = 2,
|
||||||
|
E_ACCOUNT_DISABLED = 3,
|
||||||
|
E_IPADDRESS_BLOCKED = 4,
|
||||||
|
E_ACCOUNT_BANISHED = 5,
|
||||||
|
E_IPADDRESS_BANISHED = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
// IMPORTANT(fusion): Check whether credentials are empty, in which case
|
||||||
|
// we can skip accessing the database altogether, but also prevent errors
|
||||||
|
// such as "ip-address blocked" or "account disabled" for account id ZERO.
|
||||||
|
QUERY_ERROR_IF(AccountID == 0, E_ACCOUNT_NOT_FOUND);
|
||||||
|
QUERY_ERROR_IF(StringEmpty(Password), E_PASSWORD_MISMATCH);
|
||||||
|
|
||||||
TransactionScope Tx("LoginAccount");
|
TransactionScope Tx("LoginAccount");
|
||||||
QUERY_STOP_IF(!Tx.Begin(Database));
|
QUERY_STOP_IF(!Tx.Begin(Database));
|
||||||
|
|
||||||
|
// IMPORTANT(fusion): Disallow blocked IP addresses and accounts before
|
||||||
|
// checking credentials to prevent error messages from being used as an
|
||||||
|
// oracle for brute force attacks.
|
||||||
|
int FailedLoginAttempts;
|
||||||
|
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
|
||||||
|
QUERY_ERROR_IF(FailedLoginAttempts >= 20, E_IPADDRESS_BLOCKED);
|
||||||
|
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts));
|
||||||
|
QUERY_ERROR_IF(FailedLoginAttempts >= 10, E_ACCOUNT_DISABLED);
|
||||||
|
|
||||||
TAccount Account;
|
TAccount Account;
|
||||||
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
|
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
|
||||||
QUERY_ERROR_IF(Account.AccountID == 0, 1);
|
QUERY_ERROR_IF(Account.AccountID == 0, E_ACCOUNT_NOT_FOUND);
|
||||||
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 2);
|
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), E_PASSWORD_MISMATCH);
|
||||||
|
|
||||||
int FailedLoginAttempts;
|
|
||||||
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, Account.AccountID, 5 * 60, &FailedLoginAttempts));
|
|
||||||
QUERY_ERROR_IF(FailedLoginAttempts > 10, 3);
|
|
||||||
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
|
|
||||||
QUERY_ERROR_IF(FailedLoginAttempts > 20, 4);
|
|
||||||
|
|
||||||
bool IsBanished;
|
bool IsBanished;
|
||||||
QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished));
|
QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished));
|
||||||
QUERY_ERROR_IF(IsBanished, 5);
|
QUERY_ERROR_IF(IsBanished, E_ACCOUNT_BANISHED);
|
||||||
QUERY_STOP_IF(!IsIPBanished(Database, IPAddress, &IsBanished));
|
QUERY_STOP_IF(!IsIPBanished(Database, IPAddress, &IsBanished));
|
||||||
QUERY_ERROR_IF(IsBanished, 6);
|
QUERY_ERROR_IF(IsBanished, E_IPADDRESS_BANISHED);
|
||||||
|
|
||||||
DynamicArray<TCharacterEndpoint> Characters;
|
DynamicArray<TCharacterEndpoint> Characters;
|
||||||
QUERY_STOP_IF(!GetCharacterEndpoints(Database, Account.AccountID, &Characters));
|
QUERY_STOP_IF(!GetCharacterEndpoints(Database, Account.AccountID, &Characters));
|
||||||
@ -625,40 +659,66 @@ void ProcessLoginAccount(TDatabase *Database, TQuery *Query){
|
|||||||
static void LoginGameTx(TDatabase *Database, TQuery *Query,
|
static void LoginGameTx(TDatabase *Database, TQuery *Query,
|
||||||
int AccountID, const char *CharacterName, const char *Password,
|
int AccountID, const char *CharacterName, const char *Password,
|
||||||
int IPAddress, bool PrivateWorld, bool GamemasterRequired){
|
int IPAddress, bool PrivateWorld, bool GamemasterRequired){
|
||||||
|
enum{
|
||||||
|
E_CHARACTER_NOT_FOUND = 1,
|
||||||
|
E_CHARACTER_DELETED = 2,
|
||||||
|
E_CHARACTER_WORLD_MISMATCH = 3,
|
||||||
|
E_CHARACTER_NOT_INVITED = 4,
|
||||||
|
E_PASSWORD_MISMATCH = 6,
|
||||||
|
E_ACCOUNT_DISABLED = 7,
|
||||||
|
E_ACCOUNT_DELETED = 8,
|
||||||
|
E_IPADDRESS_BLOCKED = 9,
|
||||||
|
E_ACCOUNT_BANISHED = 10,
|
||||||
|
E_CHARACTER_BANISHED = 11,
|
||||||
|
E_IPADDRESS_BANISHED = 12,
|
||||||
|
E_ACCOUNT_BUSY = 13,
|
||||||
|
E_ACCOUNT_NOT_GAMEMASTER = 14,
|
||||||
|
E_ACCOUNT_INVALID = 15,
|
||||||
|
};
|
||||||
|
|
||||||
|
// IMPORTANT(fusion): Check whether credentials are empty, in which case
|
||||||
|
// we can skip accessing the database altogether, but also prevent errors
|
||||||
|
// such as "ip-address blocked" or "account disabled" for account id ZERO.
|
||||||
|
QUERY_ERROR_IF(AccountID == 0, E_ACCOUNT_INVALID);
|
||||||
|
QUERY_ERROR_IF(StringEmpty(CharacterName), E_CHARACTER_NOT_FOUND);
|
||||||
|
QUERY_ERROR_IF(StringEmpty(Password), E_PASSWORD_MISMATCH);
|
||||||
|
|
||||||
TransactionScope Tx("LoginGame");
|
TransactionScope Tx("LoginGame");
|
||||||
QUERY_STOP_IF(!Tx.Begin(Database));
|
QUERY_STOP_IF(!Tx.Begin(Database));
|
||||||
|
|
||||||
|
// IMPORTANT(fusion): Disallow blocked IP addresses and accounts before
|
||||||
|
// checking credentials to prevent error messages from being used as an
|
||||||
|
// oracle for brute force attacks.
|
||||||
|
int FailedLoginAttempts;
|
||||||
|
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
|
||||||
|
QUERY_ERROR_IF(FailedLoginAttempts >= 20, E_IPADDRESS_BLOCKED);
|
||||||
|
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, AccountID, 5 * 60, &FailedLoginAttempts));
|
||||||
|
QUERY_ERROR_IF(FailedLoginAttempts >= 10, E_ACCOUNT_DISABLED);
|
||||||
|
|
||||||
TCharacterLoginData Character;
|
TCharacterLoginData Character;
|
||||||
QUERY_STOP_IF(!GetCharacterLoginData(Database, CharacterName, &Character));
|
QUERY_STOP_IF(!GetCharacterLoginData(Database, CharacterName, &Character));
|
||||||
QUERY_ERROR_IF(Character.CharacterID == 0, 1);
|
QUERY_ERROR_IF(Character.CharacterID == 0, E_CHARACTER_NOT_FOUND);
|
||||||
QUERY_ERROR_IF(Character.Deleted, 2);
|
QUERY_ERROR_IF(Character.Deleted, E_CHARACTER_DELETED);
|
||||||
QUERY_ERROR_IF(Character.WorldID != Query->WorldID, 3);
|
QUERY_ERROR_IF(Character.WorldID != Query->WorldID, E_CHARACTER_WORLD_MISMATCH);
|
||||||
if(PrivateWorld){
|
if(PrivateWorld){
|
||||||
bool Invited;
|
bool Invited;
|
||||||
QUERY_STOP_IF(!GetWorldInvitation(Database, Query->WorldID, Character.CharacterID, &Invited));
|
QUERY_STOP_IF(!GetWorldInvitation(Database, Query->WorldID, Character.CharacterID, &Invited));
|
||||||
QUERY_ERROR_IF(!Invited, 4);
|
QUERY_ERROR_IF(!Invited, E_CHARACTER_NOT_INVITED);
|
||||||
}
|
}
|
||||||
|
|
||||||
TAccount Account;
|
TAccount Account;
|
||||||
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
|
QUERY_STOP_IF(!GetAccountData(Database, AccountID, &Account));
|
||||||
// NOTE(fusion): This is correct, there is no error code 5.
|
QUERY_ERROR_IF(Account.AccountID == 0 || Account.AccountID != Character.AccountID, E_ACCOUNT_INVALID);
|
||||||
QUERY_ERROR_IF(Account.AccountID == 0 || Account.AccountID != Character.AccountID, 15);
|
QUERY_ERROR_IF(Account.Deleted, E_ACCOUNT_DELETED);
|
||||||
QUERY_ERROR_IF(Account.Deleted, 8);
|
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), E_PASSWORD_MISMATCH);
|
||||||
QUERY_ERROR_IF(!TestPassword(Account.Auth, sizeof(Account.Auth), Password), 6);
|
|
||||||
|
|
||||||
int FailedLoginAttempts;
|
|
||||||
QUERY_STOP_IF(!GetAccountFailedLoginAttempts(Database, Account.AccountID, 5 * 60, &FailedLoginAttempts));
|
|
||||||
QUERY_ERROR_IF(FailedLoginAttempts > 10, 7);
|
|
||||||
QUERY_STOP_IF(!GetIPAddressFailedLoginAttempts(Database, IPAddress, 30 * 60, &FailedLoginAttempts));
|
|
||||||
QUERY_ERROR_IF(FailedLoginAttempts > 20, 9);
|
|
||||||
|
|
||||||
bool IsBanished;
|
bool IsBanished;
|
||||||
QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished));
|
QUERY_STOP_IF(!IsAccountBanished(Database, Account.AccountID, &IsBanished));
|
||||||
QUERY_ERROR_IF(IsBanished, 10);
|
QUERY_ERROR_IF(IsBanished, E_ACCOUNT_BANISHED);
|
||||||
QUERY_STOP_IF(!IsCharacterNamelocked(Database, Character.CharacterID, &IsBanished));
|
QUERY_STOP_IF(!IsCharacterNamelocked(Database, Character.CharacterID, &IsBanished));
|
||||||
QUERY_ERROR_IF(IsBanished, 11);
|
QUERY_ERROR_IF(IsBanished, E_CHARACTER_BANISHED);
|
||||||
QUERY_STOP_IF(!IsIPBanished(Database, IPAddress, &IsBanished));
|
QUERY_STOP_IF(!IsIPBanished(Database, IPAddress, &IsBanished));
|
||||||
QUERY_ERROR_IF(IsBanished, 12);
|
QUERY_ERROR_IF(IsBanished, E_IPADDRESS_BANISHED);
|
||||||
|
|
||||||
// TODO(fusion): Probably merge these into a single operation?
|
// TODO(fusion): Probably merge these into a single operation?
|
||||||
bool MultiClient;
|
bool MultiClient;
|
||||||
@ -669,16 +729,22 @@ static void LoginGameTx(TDatabase *Database, TQuery *Query,
|
|||||||
if(OnlineCharacters > 0){
|
if(OnlineCharacters > 0){
|
||||||
bool IsOnline;
|
bool IsOnline;
|
||||||
QUERY_STOP_IF(!IsCharacterOnline(Database, Character.CharacterID, &IsOnline));
|
QUERY_STOP_IF(!IsCharacterOnline(Database, Character.CharacterID, &IsOnline));
|
||||||
QUERY_ERROR_IF(!IsOnline, 13);
|
QUERY_ERROR_IF(!IsOnline, E_ACCOUNT_BUSY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(fusion): This may have been related to account rights but at the moment
|
||||||
|
// we only have character specific rights. May also use `READ_GAMEMASTER_CHANNEL`
|
||||||
|
// rather than `GAMEMASTER_OUTFIT`.
|
||||||
if(GamemasterRequired){
|
if(GamemasterRequired){
|
||||||
bool GamemasterOutfit;
|
bool GamemasterOutfit;
|
||||||
QUERY_STOP_IF(!GetCharacterRight(Database, Character.CharacterID, "GAMEMASTER_OUTFIT", &GamemasterOutfit));
|
QUERY_STOP_IF(!GetCharacterRight(Database, Character.CharacterID, "GAMEMASTER_OUTFIT", &GamemasterOutfit));
|
||||||
QUERY_ERROR_IF(!GamemasterOutfit, 14);
|
QUERY_ERROR_IF(!GamemasterOutfit, E_ACCOUNT_NOT_GAMEMASTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TCharacterGuildData GuildData;
|
||||||
|
QUERY_STOP_IF(!GetCharacterGuildData(Database, Character.CharacterID, &GuildData));
|
||||||
|
|
||||||
DynamicArray<TAccountBuddy> Buddies;
|
DynamicArray<TAccountBuddy> Buddies;
|
||||||
QUERY_STOP_IF(!GetBuddies(Database, Query->WorldID, Account.AccountID, &Buddies));
|
QUERY_STOP_IF(!GetBuddies(Database, Query->WorldID, Account.AccountID, &Buddies));
|
||||||
|
|
||||||
@ -704,9 +770,9 @@ static void LoginGameTx(TDatabase *Database, TQuery *Query,
|
|||||||
Response->Write32((uint32)Character.CharacterID);
|
Response->Write32((uint32)Character.CharacterID);
|
||||||
Response->WriteString(Character.Name);
|
Response->WriteString(Character.Name);
|
||||||
Response->Write8((uint8)Character.Sex);
|
Response->Write8((uint8)Character.Sex);
|
||||||
Response->WriteString(Character.Guild);
|
Response->WriteString(GuildData.GuildName);
|
||||||
Response->WriteString(Character.Rank);
|
Response->WriteString(GuildData.RankName);
|
||||||
Response->WriteString(Character.Title);
|
Response->WriteString(GuildData.Title);
|
||||||
|
|
||||||
int NumBuddies = std::min<int>(Buddies.Length(), UINT8_MAX);
|
int NumBuddies = std::min<int>(Buddies.Length(), UINT8_MAX);
|
||||||
Response->Write8((uint8)NumBuddies);
|
Response->Write8((uint8)NumBuddies);
|
||||||
@ -773,6 +839,13 @@ void ProcessLogoutGame(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessSetNamelock(TDatabase *Database, TQuery *Query){
|
void ProcessSetNamelock(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_NOT_FOUND = 1,
|
||||||
|
E_NO_BANISHMENT = 2,
|
||||||
|
E_NAME_LOCKED = 3,
|
||||||
|
E_NAME_APPROVED = 4,
|
||||||
|
};
|
||||||
|
|
||||||
TReadBuffer Request = Query->Request;
|
TReadBuffer Request = Query->Request;
|
||||||
|
|
||||||
char CharacterName[30];
|
char CharacterName[30];
|
||||||
@ -793,16 +866,15 @@ void ProcessSetNamelock(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
int CharacterID;
|
int CharacterID;
|
||||||
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
||||||
QUERY_ERROR_IF(CharacterID == 0, 1);
|
QUERY_ERROR_IF(CharacterID == 0, E_NOT_FOUND);
|
||||||
|
|
||||||
// TODO(fusion): Might be `NO_BANISHMENT`.
|
bool NoBanishment;
|
||||||
bool Namelock;
|
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NO_BANISHMENT", &NoBanishment));
|
||||||
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NAMELOCK", &Namelock));
|
QUERY_ERROR_IF(NoBanishment, E_NO_BANISHMENT);
|
||||||
QUERY_ERROR_IF(Namelock, 2);
|
|
||||||
|
|
||||||
TNamelockStatus Status;
|
TNamelockStatus Status;
|
||||||
QUERY_STOP_IF(!GetNamelockStatus(Database, CharacterID, &Status));
|
QUERY_STOP_IF(!GetNamelockStatus(Database, CharacterID, &Status));
|
||||||
QUERY_ERROR_IF(Status.Namelocked, (Status.Approved ? 4 : 3));
|
QUERY_ERROR_IF(Status.Namelocked, (Status.Approved ? E_NAME_APPROVED : E_NAME_LOCKED));
|
||||||
|
|
||||||
QUERY_STOP_IF(!InsertNamelock(Database, CharacterID, IPAddress, GamemasterID, Reason, Comment));
|
QUERY_STOP_IF(!InsertNamelock(Database, CharacterID, IPAddress, GamemasterID, Reason, Comment));
|
||||||
QUERY_STOP_IF(!Tx.Commit());
|
QUERY_STOP_IF(!Tx.Commit());
|
||||||
@ -810,6 +882,12 @@ void ProcessSetNamelock(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessBanishAccount(TDatabase *Database, TQuery *Query){
|
void ProcessBanishAccount(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_NOT_FOUND = 1,
|
||||||
|
E_NO_BANISHMENT = 2,
|
||||||
|
E_BANISHED = 3,
|
||||||
|
};
|
||||||
|
|
||||||
TReadBuffer Request = Query->Request;
|
TReadBuffer Request = Query->Request;
|
||||||
|
|
||||||
char CharacterName[30];
|
char CharacterName[30];
|
||||||
@ -831,16 +909,15 @@ void ProcessBanishAccount(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
int CharacterID;
|
int CharacterID;
|
||||||
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
||||||
QUERY_ERROR_IF(CharacterID == 0, 1);
|
QUERY_ERROR_IF(CharacterID == 0, E_NOT_FOUND);
|
||||||
|
|
||||||
// TODO(fusion): Might be `NO_BANISHMENT`.
|
bool NoBanishment;
|
||||||
bool Banishment;
|
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NO_BANISHMENT", &NoBanishment));
|
||||||
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "BANISHMENT", &Banishment));
|
QUERY_ERROR_IF(NoBanishment, E_NO_BANISHMENT);
|
||||||
QUERY_ERROR_IF(Banishment, 2);
|
|
||||||
|
|
||||||
TBanishmentStatus Status;
|
TBanishmentStatus Status;
|
||||||
QUERY_STOP_IF(!GetBanishmentStatus(Database, CharacterID, &Status));
|
QUERY_STOP_IF(!GetBanishmentStatus(Database, CharacterID, &Status));
|
||||||
QUERY_ERROR_IF(Status.Banished, 3);
|
QUERY_ERROR_IF(Status.Banished, E_BANISHED);
|
||||||
|
|
||||||
int Days = 7;
|
int Days = 7;
|
||||||
int BanishmentID = 0;
|
int BanishmentID = 0;
|
||||||
@ -858,6 +935,11 @@ void ProcessBanishAccount(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessSetNotation(TDatabase *Database, TQuery *Query){
|
void ProcessSetNotation(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_NOT_FOUND = 1,
|
||||||
|
E_NO_BANISHMENT = 2,
|
||||||
|
};
|
||||||
|
|
||||||
TReadBuffer Request = Query->Request;
|
TReadBuffer Request = Query->Request;
|
||||||
|
|
||||||
char CharacterName[30];
|
char CharacterName[30];
|
||||||
@ -878,12 +960,11 @@ void ProcessSetNotation(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
int CharacterID;
|
int CharacterID;
|
||||||
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
||||||
QUERY_ERROR_IF(CharacterID == 0, 1);
|
QUERY_ERROR_IF(CharacterID == 0, E_NOT_FOUND);
|
||||||
|
|
||||||
// TODO(fusion): Might be `NO_BANISHMENT`.
|
bool NoBanishment;
|
||||||
bool Notation;
|
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NO_BANISHMENT", &NoBanishment));
|
||||||
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NOTATION", &Notation));
|
QUERY_ERROR_IF(NoBanishment, E_NO_BANISHMENT);
|
||||||
QUERY_ERROR_IF(Notation, 2);
|
|
||||||
|
|
||||||
int Notations = 0;
|
int Notations = 0;
|
||||||
int BanishmentID = 0;
|
int BanishmentID = 0;
|
||||||
@ -908,6 +989,11 @@ void ProcessSetNotation(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessReportStatement(TDatabase *Database, TQuery *Query){
|
void ProcessReportStatement(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_NOT_FOUND = 1,
|
||||||
|
E_REPORTED = 2,
|
||||||
|
};
|
||||||
|
|
||||||
TReadBuffer Request = Query->Request;
|
TReadBuffer Request = Query->Request;
|
||||||
|
|
||||||
char CharacterName[30];
|
char CharacterName[30];
|
||||||
@ -962,7 +1048,7 @@ void ProcessReportStatement(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
int CharacterID;
|
int CharacterID;
|
||||||
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
||||||
QUERY_ERROR_IF(CharacterID == 0, 1);
|
QUERY_ERROR_IF(CharacterID == 0, E_NOT_FOUND);
|
||||||
|
|
||||||
if(ReportedStatement->CharacterID != CharacterID){
|
if(ReportedStatement->CharacterID != CharacterID){
|
||||||
LOG_ERR("Reported statement character mismatch");
|
LOG_ERR("Reported statement character mismatch");
|
||||||
@ -972,7 +1058,7 @@ void ProcessReportStatement(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
bool IsReported;
|
bool IsReported;
|
||||||
QUERY_STOP_IF(!IsStatementReported(Database, Query->WorldID, ReportedStatement, &IsReported));
|
QUERY_STOP_IF(!IsStatementReported(Database, Query->WorldID, ReportedStatement, &IsReported));
|
||||||
QUERY_ERROR_IF(IsReported, 2);
|
QUERY_ERROR_IF(IsReported, E_REPORTED);
|
||||||
|
|
||||||
QUERY_STOP_IF(!InsertStatements(Database, Query->WorldID, NumStatements, Statements));
|
QUERY_STOP_IF(!InsertStatements(Database, Query->WorldID, NumStatements, Statements));
|
||||||
QUERY_STOP_IF(!InsertReportedStatement(Database, Query->WorldID, ReportedStatement,
|
QUERY_STOP_IF(!InsertReportedStatement(Database, Query->WorldID, ReportedStatement,
|
||||||
@ -982,6 +1068,11 @@ void ProcessReportStatement(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessBanishIpAddress(TDatabase *Database, TQuery *Query){
|
void ProcessBanishIpAddress(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_NOT_FOUND = 1,
|
||||||
|
E_NO_BANISHMENT = 2,
|
||||||
|
};
|
||||||
|
|
||||||
TReadBuffer Request = Query->Request;
|
TReadBuffer Request = Query->Request;
|
||||||
|
|
||||||
char CharacterName[30];
|
char CharacterName[30];
|
||||||
@ -1002,12 +1093,11 @@ void ProcessBanishIpAddress(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
int CharacterID;
|
int CharacterID;
|
||||||
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
QUERY_STOP_IF(!GetCharacterID(Database, Query->WorldID, CharacterName, &CharacterID));
|
||||||
QUERY_ERROR_IF(CharacterID == 0, 1);
|
QUERY_ERROR_IF(CharacterID == 0, E_NOT_FOUND);
|
||||||
|
|
||||||
// TODO(fusion): Might be `NO_BANISHMENT`.
|
bool NoBanishment;
|
||||||
bool IPBanishment;
|
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "NO_BANISHMENT", &NoBanishment));
|
||||||
QUERY_STOP_IF(!GetCharacterRight(Database, CharacterID, "IP_BANISHMENT", &IPBanishment));
|
QUERY_ERROR_IF(NoBanishment, E_NO_BANISHMENT);
|
||||||
QUERY_ERROR_IF(IPBanishment, 2);
|
|
||||||
|
|
||||||
// IMPORTANT(fusion): It is not a good idea to ban IP addresses, specially V4,
|
// IMPORTANT(fusion): It is not a good idea to ban IP addresses, specially V4,
|
||||||
// as they may be dynamically assigned or represent the address of a public ISP
|
// as they may be dynamically assigned or represent the address of a public ISP
|
||||||
@ -1131,7 +1221,7 @@ void ProcessEvictExGuildleaders(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
bool IsGuildLeader;
|
bool IsGuildLeader;
|
||||||
QUERY_STOP_IF(!GetGuildLeaderStatus(Database, Query->WorldID, OwnerID, &IsGuildLeader));
|
QUERY_STOP_IF(!GetGuildLeaderStatus(Database, Query->WorldID, OwnerID, &IsGuildLeader));
|
||||||
if(IsGuildLeader){
|
if(!IsGuildLeader){
|
||||||
Evictions.Push(HouseID);
|
Evictions.Push(HouseID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1371,6 +1461,11 @@ void ProcessLoadWorldConfig(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessCreateAccount(TDatabase *Database, TQuery *Query){
|
void ProcessCreateAccount(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_ACCOUNT_NUMBER_EXISTS = 1,
|
||||||
|
E_ACCOUNT_EMAIL_EXISTS = 2,
|
||||||
|
};
|
||||||
|
|
||||||
// TODO(fusion): We'd ideally want to automatically generate an account number
|
// TODO(fusion): We'd ideally want to automatically generate an account number
|
||||||
// and return it in case of success but that would also require a more robust
|
// and return it in case of success but that would also require a more robust
|
||||||
// website infrastructure with verification e-mails, etc...
|
// website infrastructure with verification e-mails, etc...
|
||||||
@ -1394,9 +1489,9 @@ void ProcessCreateAccount(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
bool AccountExists;
|
bool AccountExists;
|
||||||
QUERY_STOP_IF(!AccountNumberExists(Database, AccountID, &AccountExists));
|
QUERY_STOP_IF(!AccountNumberExists(Database, AccountID, &AccountExists));
|
||||||
QUERY_ERROR_IF(AccountExists, 1);
|
QUERY_ERROR_IF(AccountExists, E_ACCOUNT_NUMBER_EXISTS);
|
||||||
QUERY_STOP_IF(!AccountEmailExists(Database, Email, &AccountExists));
|
QUERY_STOP_IF(!AccountEmailExists(Database, Email, &AccountExists));
|
||||||
QUERY_ERROR_IF(AccountExists, 2);
|
QUERY_ERROR_IF(AccountExists, E_ACCOUNT_EMAIL_EXISTS);
|
||||||
|
|
||||||
QUERY_STOP_IF(!CreateAccount(Database, AccountID, Email, Auth, sizeof(Auth)));
|
QUERY_STOP_IF(!CreateAccount(Database, AccountID, Email, Auth, sizeof(Auth)));
|
||||||
QUERY_STOP_IF(!Tx.Commit());
|
QUERY_STOP_IF(!Tx.Commit());
|
||||||
@ -1404,6 +1499,12 @@ void ProcessCreateAccount(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessCreateCharacter(TDatabase *Database, TQuery *Query){
|
void ProcessCreateCharacter(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_WORLD_NOT_FOUND = 1,
|
||||||
|
E_ACCOUNT_NOT_FOUND = 2,
|
||||||
|
E_CHARACTER_NAME_EXISTS = 3,
|
||||||
|
};
|
||||||
|
|
||||||
char WorldName[30];
|
char WorldName[30];
|
||||||
char CharacterName[30];
|
char CharacterName[30];
|
||||||
TReadBuffer Request = Query->Request;
|
TReadBuffer Request = Query->Request;
|
||||||
@ -1423,15 +1524,15 @@ void ProcessCreateCharacter(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
int WorldID;
|
int WorldID;
|
||||||
QUERY_STOP_IF(!GetWorldID(Database, WorldName, &WorldID));
|
QUERY_STOP_IF(!GetWorldID(Database, WorldName, &WorldID));
|
||||||
QUERY_ERROR_IF(WorldID == 0, 1);
|
QUERY_ERROR_IF(WorldID == 0, E_WORLD_NOT_FOUND);
|
||||||
|
|
||||||
bool AccountExists;
|
bool AccountExists;
|
||||||
QUERY_STOP_IF(!AccountNumberExists(Database, AccountID, &AccountExists));
|
QUERY_STOP_IF(!AccountNumberExists(Database, AccountID, &AccountExists));
|
||||||
QUERY_ERROR_IF(!AccountExists, 2);
|
QUERY_ERROR_IF(!AccountExists, E_ACCOUNT_NOT_FOUND);
|
||||||
|
|
||||||
bool CharacterExists;
|
bool CharacterExists;
|
||||||
QUERY_STOP_IF(!CharacterNameExists(Database, CharacterName, &CharacterExists));
|
QUERY_STOP_IF(!CharacterNameExists(Database, CharacterName, &CharacterExists));
|
||||||
QUERY_ERROR_IF(CharacterExists, 3);
|
QUERY_ERROR_IF(CharacterExists, E_CHARACTER_NAME_EXISTS);
|
||||||
|
|
||||||
QUERY_STOP_IF(!CreateCharacter(Database, WorldID, AccountID, CharacterName, Sex));
|
QUERY_STOP_IF(!CreateCharacter(Database, WorldID, AccountID, CharacterName, Sex));
|
||||||
QUERY_STOP_IF(!Tx.Commit());
|
QUERY_STOP_IF(!Tx.Commit());
|
||||||
@ -1470,6 +1571,10 @@ void ProcessGetAccountSummary(TDatabase *Database, TQuery *Query){
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProcessGetCharacterProfile(TDatabase *Database, TQuery *Query){
|
void ProcessGetCharacterProfile(TDatabase *Database, TQuery *Query){
|
||||||
|
enum{
|
||||||
|
E_CHARACTER_NOT_FOUND = 1,
|
||||||
|
};
|
||||||
|
|
||||||
char CharacterName[30];
|
char CharacterName[30];
|
||||||
TReadBuffer Request = Query->Request;
|
TReadBuffer Request = Query->Request;
|
||||||
Request.ReadString(CharacterName, sizeof(CharacterName));
|
Request.ReadString(CharacterName, sizeof(CharacterName));
|
||||||
@ -1478,15 +1583,18 @@ void ProcessGetCharacterProfile(TDatabase *Database, TQuery *Query){
|
|||||||
|
|
||||||
TCharacterProfile Character;
|
TCharacterProfile Character;
|
||||||
QUERY_STOP_IF(!GetCharacterProfile(Database, CharacterName, &Character));
|
QUERY_STOP_IF(!GetCharacterProfile(Database, CharacterName, &Character));
|
||||||
QUERY_ERROR_IF(!StringEqCI(Character.Name, CharacterName), 1);
|
QUERY_ERROR_IF(Character.CharacterID == 0, E_CHARACTER_NOT_FOUND);
|
||||||
|
|
||||||
|
TCharacterGuildData GuildData;
|
||||||
|
QUERY_STOP_IF(!GetCharacterGuildData(Database, Character.CharacterID, &GuildData));
|
||||||
|
|
||||||
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
|
TWriteBuffer *Response = QueryBeginResponse(Query, QUERY_STATUS_OK);
|
||||||
Response->WriteString(Character.Name);
|
Response->WriteString(Character.Name);
|
||||||
Response->WriteString(Character.World);
|
Response->WriteString(Character.World);
|
||||||
Response->Write8((uint8)Character.Sex);
|
Response->Write8((uint8)Character.Sex);
|
||||||
Response->WriteString(Character.Guild);
|
Response->WriteString(GuildData.GuildName);
|
||||||
Response->WriteString(Character.Rank);
|
Response->WriteString(GuildData.RankName);
|
||||||
Response->WriteString(Character.Title);
|
Response->WriteString(GuildData.Title);
|
||||||
Response->Write16((uint16)Character.Level);
|
Response->Write16((uint16)Character.Level);
|
||||||
Response->WriteString(Character.Profession);
|
Response->WriteString(Character.Profession);
|
||||||
Response->WriteString(Character.Residence);
|
Response->WriteString(Character.Residence);
|
||||||
|
|||||||
@ -240,12 +240,12 @@ bool StringFormat(char *Dest, int DestCapacity, const char *Format, ...){
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Timestamp){
|
bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Timestamp){
|
||||||
struct tm tm = GetLocalTime((int)Timestamp);
|
struct tm tm = GetLocalTime((time_t)Timestamp);
|
||||||
int Result = (int)strftime(Dest, DestCapacity, Format, &tm);
|
int Result = (int)strftime(Dest, DestCapacity, Format, &tm);
|
||||||
|
|
||||||
// NOTE(fusion): `strftime` will should return ZERO if it's unable to fit
|
// NOTE(fusion): `strftime` will return ZERO if it's unable to fit the result
|
||||||
// the result in the supplied buffer, which is annoying because ZERO may
|
// in the supplied buffer, which is annoying because ZERO may not represent a
|
||||||
// not represent a failure if the result is an empty string.
|
// failure if the result is an empty string.
|
||||||
ASSERT(Result >= 0 && Result < DestCapacity);
|
ASSERT(Result >= 0 && Result < DestCapacity);
|
||||||
if(Result == 0){
|
if(Result == 0){
|
||||||
memset(Dest, 0, DestCapacity);
|
memset(Dest, 0, DestCapacity);
|
||||||
@ -704,20 +704,20 @@ bool ReadConfig(const char *FileName, TConfig *Config){
|
|||||||
ParseStringBuf(Config->PostgreSQL.SSLRootCert, Val);
|
ParseStringBuf(Config->PostgreSQL.SSLRootCert, Val);
|
||||||
}else if(StringEqCI(Key, "PostgreSQL.MaxCachedStatements")){
|
}else if(StringEqCI(Key, "PostgreSQL.MaxCachedStatements")){
|
||||||
ParseInteger(&Config->PostgreSQL.MaxCachedStatements, Val);
|
ParseInteger(&Config->PostgreSQL.MaxCachedStatements, Val);
|
||||||
}else if(StringEqCI(Key, "MySQL.Host")){
|
}else if(StringEqCI(Key, "MariaDB.Host")){
|
||||||
ParseStringBuf(Config->MySQL.Host, Val);
|
ParseStringBuf(Config->MariaDB.Host, Val);
|
||||||
}else if(StringEqCI(Key, "MySQL.Port")){
|
}else if(StringEqCI(Key, "MariaDB.Port")){
|
||||||
ParseStringBuf(Config->MySQL.Port, Val);
|
ParseStringBuf(Config->MariaDB.Port, Val);
|
||||||
}else if(StringEqCI(Key, "MySQL.DBName")){
|
}else if(StringEqCI(Key, "MariaDB.DBName")){
|
||||||
ParseStringBuf(Config->MySQL.DBName, Val);
|
ParseStringBuf(Config->MariaDB.DBName, Val);
|
||||||
}else if(StringEqCI(Key, "MySQL.User")){
|
}else if(StringEqCI(Key, "MariaDB.User")){
|
||||||
ParseStringBuf(Config->MySQL.User, Val);
|
ParseStringBuf(Config->MariaDB.User, Val);
|
||||||
}else if(StringEqCI(Key, "MySQL.Password")){
|
}else if(StringEqCI(Key, "MariaDB.Password")){
|
||||||
ParseStringBuf(Config->MySQL.Password, Val);
|
ParseStringBuf(Config->MariaDB.Password, Val);
|
||||||
}else if(StringEqCI(Key, "MySQL.UnixSocket")){
|
}else if(StringEqCI(Key, "MariaDB.UnixSocket")){
|
||||||
ParseStringBuf(Config->MySQL.UnixSocket, Val);
|
ParseStringBuf(Config->MariaDB.UnixSocket, Val);
|
||||||
}else if(StringEqCI(Key, "MySQL.MaxCachedStatements")){
|
}else if(StringEqCI(Key, "MariaDB.MaxCachedStatements")){
|
||||||
ParseInteger(&Config->MySQL.MaxCachedStatements, Val);
|
ParseInteger(&Config->MariaDB.MaxCachedStatements, Val);
|
||||||
}else if(StringEqCI(Key, "QueryManagerPort")){
|
}else if(StringEqCI(Key, "QueryManagerPort")){
|
||||||
ParseInteger(&Config->QueryManagerPort, Val);
|
ParseInteger(&Config->QueryManagerPort, Val);
|
||||||
}else if(StringEqCI(Key, "QueryManagerPassword")){
|
}else if(StringEqCI(Key, "QueryManagerPassword")){
|
||||||
@ -780,8 +780,8 @@ int main(int argc, const char **argv){
|
|||||||
g_Config.SQLite.MaxCachedStatements = 100;
|
g_Config.SQLite.MaxCachedStatements = 100;
|
||||||
|
|
||||||
// PostgreSQL Config
|
// PostgreSQL Config
|
||||||
StringBufCopy(g_Config.PostgreSQL.Host, "localhost");
|
StringBufCopy(g_Config.PostgreSQL.Host, "");
|
||||||
StringBufCopy(g_Config.PostgreSQL.Port, "5432");
|
StringBufCopy(g_Config.PostgreSQL.Port, "");
|
||||||
StringBufCopy(g_Config.PostgreSQL.DBName, "tibia");
|
StringBufCopy(g_Config.PostgreSQL.DBName, "tibia");
|
||||||
StringBufCopy(g_Config.PostgreSQL.User, "tibia");
|
StringBufCopy(g_Config.PostgreSQL.User, "tibia");
|
||||||
StringBufCopy(g_Config.PostgreSQL.Password, "");
|
StringBufCopy(g_Config.PostgreSQL.Password, "");
|
||||||
@ -791,17 +791,17 @@ int main(int argc, const char **argv){
|
|||||||
StringBufCopy(g_Config.PostgreSQL.SSLRootCert, "");
|
StringBufCopy(g_Config.PostgreSQL.SSLRootCert, "");
|
||||||
g_Config.PostgreSQL.MaxCachedStatements = 100;
|
g_Config.PostgreSQL.MaxCachedStatements = 100;
|
||||||
|
|
||||||
// MySQL/MariaDB Config
|
// MariaDB Config
|
||||||
StringBufCopy(g_Config.MySQL.Host, "localhost");
|
StringBufCopy(g_Config.MariaDB.Host, "localhost");
|
||||||
StringBufCopy(g_Config.MySQL.Port, "3306");
|
StringBufCopy(g_Config.MariaDB.Port, "3306");
|
||||||
StringBufCopy(g_Config.MySQL.DBName, "tibia");
|
StringBufCopy(g_Config.MariaDB.DBName, "tibia");
|
||||||
StringBufCopy(g_Config.MySQL.User, "tibia");
|
StringBufCopy(g_Config.MariaDB.User, "tibia");
|
||||||
StringBufCopy(g_Config.MySQL.Password, "");
|
StringBufCopy(g_Config.MariaDB.Password, "");
|
||||||
StringBufCopy(g_Config.MySQL.UnixSocket, "");
|
StringBufCopy(g_Config.MariaDB.UnixSocket, "");
|
||||||
g_Config.MySQL.MaxCachedStatements = 100;
|
g_Config.MariaDB.MaxCachedStatements = 100;
|
||||||
|
|
||||||
// Connection Config
|
// Connection Config
|
||||||
g_Config.QueryManagerPort = 7174;
|
g_Config.QueryManagerPort = 7173;
|
||||||
StringBufCopy(g_Config.QueryManagerPassword, "");
|
StringBufCopy(g_Config.QueryManagerPassword, "");
|
||||||
g_Config.QueryWorkerThreads = 1;
|
g_Config.QueryWorkerThreads = 1;
|
||||||
g_Config.QueryBufferSize = (int)MB(1);
|
g_Config.QueryBufferSize = (int)MB(1);
|
||||||
@ -809,7 +809,7 @@ int main(int argc, const char **argv){
|
|||||||
g_Config.MaxConnections = 25;
|
g_Config.MaxConnections = 25;
|
||||||
g_Config.MaxConnectionIdleTime = 60 * 5; // seconds
|
g_Config.MaxConnectionIdleTime = 60 * 5; // seconds
|
||||||
|
|
||||||
LOG("Tibia Query Manager v0.2 (%s)", DATABASE_SYSTEM_NAME);
|
LOG("Tibia Query Manager v0.3 (%s)", DATABASE_SYSTEM_NAME);
|
||||||
if(!ReadConfig("config.cfg", &g_Config)){
|
if(!ReadConfig("config.cfg", &g_Config)){
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
@ -830,13 +830,13 @@ int main(int argc, const char **argv){
|
|||||||
LOG("PostgreSQL sslmode: \"%s\"", g_Config.PostgreSQL.SSLMode);
|
LOG("PostgreSQL sslmode: \"%s\"", g_Config.PostgreSQL.SSLMode);
|
||||||
LOG("PostgreSQL sslrootcert: \"%s\"", g_Config.PostgreSQL.SSLRootCert);
|
LOG("PostgreSQL sslrootcert: \"%s\"", g_Config.PostgreSQL.SSLRootCert);
|
||||||
LOG("PostgreSQL max cached statements: %d", g_Config.PostgreSQL.MaxCachedStatements);
|
LOG("PostgreSQL max cached statements: %d", g_Config.PostgreSQL.MaxCachedStatements);
|
||||||
#elif DATABASE_MYSQL
|
#elif DATABASE_MARIADB
|
||||||
LOG("MySQL host: \"%s\"", g_Config.MySQL.Host);
|
LOG("MariaDB host: \"%s\"", g_Config.MariaDB.Host);
|
||||||
LOG("MySQL port: \"%s\"", g_Config.MySQL.Port);
|
LOG("MariaDB port: \"%s\"", g_Config.MariaDB.Port);
|
||||||
LOG("MySQL dbname: \"%s\"", g_Config.MySQL.DBName);
|
LOG("MariaDB dbname: \"%s\"", g_Config.MariaDB.DBName);
|
||||||
LOG("MySQL user: \"%s\"", g_Config.MySQL.User);
|
LOG("MariaDB user: \"%s\"", g_Config.MariaDB.User);
|
||||||
LOG("MySQL unix socket: \"%s\"", g_Config.MySQL.UnixSocket);
|
LOG("MariaDB unix socket: \"%s\"", g_Config.MariaDB.UnixSocket);
|
||||||
LOG("MySQL max cached statements: %d", g_Config.MySQL.MaxCachedStatements);
|
LOG("MariaDB max cached statements: %d", g_Config.MariaDB.MaxCachedStatements);
|
||||||
#endif
|
#endif
|
||||||
LOG("Query manager port: %d", g_Config.QueryManagerPort);
|
LOG("Query manager port: %d", g_Config.QueryManagerPort);
|
||||||
LOG("Query worker threads: %d", g_Config.QueryWorkerThreads);
|
LOG("Query worker threads: %d", g_Config.QueryWorkerThreads);
|
||||||
|
|||||||
@ -76,9 +76,9 @@ typedef size_t usize;
|
|||||||
TRAP(); \
|
TRAP(); \
|
||||||
}while(0)
|
}while(0)
|
||||||
|
|
||||||
#if (DATABASE_SQLITE + DATABASE_POSTGRESQL + DATABASE_MYSQL) == 0
|
#if (DATABASE_SQLITE + DATABASE_POSTGRESQL + DATABASE_MARIADB) == 0
|
||||||
# error "No database system defined."
|
# error "No database system defined."
|
||||||
#elif (DATABASE_SQLITE + DATABASE_POSTGRESQL + DATABASE_MYSQL) > 1
|
#elif (DATABASE_SQLITE + DATABASE_POSTGRESQL + DATABASE_MARIADB) > 1
|
||||||
# error "Multiple database systems defined."
|
# error "Multiple database systems defined."
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -86,8 +86,8 @@ typedef size_t usize;
|
|||||||
# define DATABASE_SYSTEM_NAME "SQLite"
|
# define DATABASE_SYSTEM_NAME "SQLite"
|
||||||
#elif DATABASE_POSTGRESQL
|
#elif DATABASE_POSTGRESQL
|
||||||
# define DATABASE_SYSTEM_NAME "PostgreSQL"
|
# define DATABASE_SYSTEM_NAME "PostgreSQL"
|
||||||
#elif DATABASE_MYSQL
|
#elif DATABASE_MARIADB
|
||||||
# define DATABASE_SYSTEM_NAME "MySQL"
|
# define DATABASE_SYSTEM_NAME "MariaDB"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct TConfig{
|
struct TConfig{
|
||||||
@ -117,7 +117,7 @@ struct TConfig{
|
|||||||
int MaxCachedStatements;
|
int MaxCachedStatements;
|
||||||
} PostgreSQL;
|
} PostgreSQL;
|
||||||
|
|
||||||
// MySQL/MariaDB Config
|
// MariaDB Config
|
||||||
struct{
|
struct{
|
||||||
char Host[100];
|
char Host[100];
|
||||||
char Port[30];
|
char Port[30];
|
||||||
@ -126,7 +126,7 @@ struct TConfig{
|
|||||||
char Password[30];
|
char Password[30];
|
||||||
char UnixSocket[100];
|
char UnixSocket[100];
|
||||||
int MaxCachedStatements;
|
int MaxCachedStatements;
|
||||||
} MySQL;
|
} MariaDB;
|
||||||
|
|
||||||
// Connection Config
|
// Connection Config
|
||||||
int QueryManagerPort;
|
int QueryManagerPort;
|
||||||
@ -884,19 +884,22 @@ struct TCharacterLoginData{
|
|||||||
int AccountID;
|
int AccountID;
|
||||||
char Name[30];
|
char Name[30];
|
||||||
int Sex;
|
int Sex;
|
||||||
char Guild[30];
|
|
||||||
char Rank[30];
|
|
||||||
char Title[30];
|
|
||||||
bool Deleted;
|
bool Deleted;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TCharacterGuildData{
|
||||||
|
int GuildID;
|
||||||
|
int Rank;
|
||||||
|
char GuildName[30];
|
||||||
|
char RankName[30];
|
||||||
|
char Title[30];
|
||||||
|
};
|
||||||
|
|
||||||
struct TCharacterProfile{
|
struct TCharacterProfile{
|
||||||
|
int CharacterID;
|
||||||
char Name[30];
|
char Name[30];
|
||||||
char World[30];
|
char World[30];
|
||||||
int Sex;
|
int Sex;
|
||||||
char Guild[30];
|
|
||||||
char Rank[30];
|
|
||||||
char Title[30];
|
|
||||||
int Level;
|
int Level;
|
||||||
char Profession[30];
|
char Profession[30];
|
||||||
char Residence[30];
|
char Residence[30];
|
||||||
@ -1048,6 +1051,9 @@ bool InsertLoginAttempt(TDatabase *Database, int AccountID, int IPAddress, bool
|
|||||||
bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *FailedAttempts);
|
bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *FailedAttempts);
|
||||||
bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *FailedAttempts);
|
bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *FailedAttempts);
|
||||||
|
|
||||||
|
// NOTE(fusion): Guild Tables
|
||||||
|
bool GetCharacterGuildData(TDatabase *Database, int CharacterID, TCharacterGuildData *GuildData);
|
||||||
|
|
||||||
// NOTE(fusion): House Tables
|
// NOTE(fusion): House Tables
|
||||||
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions);
|
bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray<THouseAuction> *Auctions);
|
||||||
bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray<THouseTransfer> *Transfers);
|
bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray<THouseTransfer> *Transfers);
|
||||||
@ -1228,11 +1234,11 @@ enum : int {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum ConnectionState: int {
|
enum ConnectionState: int {
|
||||||
CONNECTION_FREE = 0,
|
CONNECTION_FREE = 0,
|
||||||
CONNECTION_READING = 1,
|
CONNECTION_READING = 1,
|
||||||
CONNECTION_REQUEST = 2,
|
CONNECTION_REQUEST = 2,
|
||||||
CONNECTION_RESPONSE = 3,
|
CONNECTION_RESPONSE = 3,
|
||||||
CONNECTION_WRITING = 4,
|
CONNECTION_WRITING = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TConnection{
|
struct TConnection{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user