summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GNUmakefile35
-rw-r--r--sql/login.sql63
-rw-r--r--src/db.c153
-rw-r--r--src/db.h49
-rw-r--r--src/game.c629
-rw-r--r--src/game.h195
-rw-r--r--src/login.c362
-rw-r--r--src/mitm.c243
-rw-r--r--src/packet.c167
-rw-r--r--src/packet.h535
-rw-r--r--src/sw-crypt.c25
-rw-r--r--src/xor.c20
-rw-r--r--tools/find-client-packets.bash31
-rw-r--r--tools/vbatch2c.py36
14 files changed, 2543 insertions, 0 deletions
diff --git a/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000..88c65c1
--- /dev/null
+++ b/GNUmakefile
@@ -0,0 +1,35 @@
+MAKEFLAGS += --no-builtin-rules
+
+WARNINGS = -Wall -Wextra -Wpedantic -Wformat=2 -Wstrict-aliasing=3 -Wstrict-overflow=3 -Wstack-usage=12500 \
+ -Wfloat-equal -Wcast-align -Wpointer-arith -Wchar-subscripts -Warray-bounds=2
+
+override CFLAGS ?= -g -O2 $(WARNINGS)
+override CFLAGS += -std=c99
+override CPPFLAGS ?= -D_FORTIFY_SOURCE=2
+
+bins = xor mitm sw-crypt login game
+all: $(bins)
+
+%.a:
+ $(LINK.c) -c $(filter %.c,$^) $(LDLIBS) -o $@
+
+$(bins): %:
+ $(LINK.c) $(filter %.c %.a,$^) $(LDLIBS) -o $@
+
+packet.a: src/packet.c src/packet.h
+db.a: src/db.c src/db.h
+
+xor: src/xor.c
+mitm: src/mitm.c packet.a
+sw-crypt: src/sw-crypt.c packet.a
+
+login: private LDLIBS += $(shell pkg-config --libs-only-l sqlite3)
+login: src/login.c db.a packet.a
+
+game: private LDLIBS += $(shell pkg-config --libs-only-l sqlite3)
+game: src/game.c src/game.h db.a packet.a
+
+clean:
+ $(RM) $(bins) *.a
+
+.PHONY: all clean
diff --git a/sql/login.sql b/sql/login.sql
new file mode 100644
index 0000000..0ada114
--- /dev/null
+++ b/sql/login.sql
@@ -0,0 +1,63 @@
+drop table if exists servers;
+create table servers (
+ id integer primary key autoincrement,
+ name char(15) not null,
+ host char(15) not null,
+ port smallint not null,
+ -- 0: MAINTENANCE
+ -- 1: GOOD
+ -- 2: NORMAL
+ -- 3: OVERLOADED
+ -- 4: QUEUE
+ status tinyint not null check (status <= 4)
+);
+
+insert into servers values (null, "login", "84.248.5.167", 10000, 1);
+insert into servers values (null, "BENIS", "84.248.5.167", 10100, 1);
+insert into servers values (null, "maintenance", "84.248.5.167", 10100, 0);
+insert into servers values (null, "normal", "84.248.5.167", 10100, 2);
+insert into servers values (null, "overloaded", "84.248.5.167", 10100, 3);
+insert into servers values (null, "queue", "84.248.5.167", 10100, 4);
+
+drop table if exists accounts;
+create table accounts (
+ id integer primary key autoincrement,
+ username varchar(16) not null,
+ password varchar(64) not null,
+ email varchar(64) not null,
+ last_login datetime default null,
+ created datetime not null default current_timestamp
+);
+
+insert into accounts values (null, "benis", "benis", "", null, current_timestamp);
+
+drop table if exists sessions;
+create table sessions (
+ id integer primary key not null check (id < 0xffffffff),
+ account_id integer not null,
+ char_id integer,
+ server char(15) not null,
+ created datetime not null default current_timestamp
+);
+
+drop table if exists characters;
+create table characters (
+ id integer primary key autoincrement check (id < 0xffffffff),
+ account_id integer not null,
+ last_login datetime,
+ created datetime not null default current_timestamp,
+ -- 1: Haru
+ -- 2: Ed
+ -- 3: Lily
+ -- 4: Jin
+ -- 5: Stella
+ -- 6: Iris
+ -- 7: Chi
+ model tinyint not null check (id > 0 and id <= 7),
+ name char(12) not null,
+ evolution tinyint not null default 0,
+ lvl tinyint not null default 1 check (lvl > 0)
+);
+
+insert into characters values (null, 1, null, current_timestamp, 6, "Iris", 1, 255);
+insert into characters values (null, 1, null, current_timestamp, 3, "Lily", 1, 255);
diff --git a/src/db.c b/src/db.c
new file mode 100644
index 0000000..76ebaf7
--- /dev/null
+++ b/src/db.c
@@ -0,0 +1,153 @@
+#include "db.h"
+#include <stdlib.h>
+#include <string.h>
+#include <err.h>
+#include <assert.h>
+
+#include <sqlite3.h>
+#include <unistd.h>
+
+static int
+busy_handler(void *dummy, int times_invoked)
+{
+ (void)dummy;
+ uint32_t sleep_s = (times_invoked > 4 ? 4 : times_invoked) + 1;
+ warnx("sqlite3 db is busy :(... sleeping for %u seconds", sleep_s);
+ sleep(sleep_s);
+ return 1;
+}
+
+void
+db_init(struct db *db, const char *target)
+{
+ assert(db && target);
+ *db = (struct db){0};
+
+ if (sqlite3_open(target, (sqlite3**)&db->handle) != SQLITE_OK)
+ errx(EXIT_FAILURE, "sqlite3_open: %s", sqlite3_errmsg(db->handle));
+
+ if (sqlite3_busy_handler(db->handle, busy_handler, NULL) != SQLITE_OK)
+ errx(EXIT_FAILURE, "sqlite3_busy_handler");
+}
+
+static struct db_query_arg
+stmt_column_to_arg(sqlite3_stmt *stmt, const int index)
+{
+ assert(stmt);
+ switch (sqlite3_column_type(stmt, index)) {
+ case SQLITE_NULL:
+ return (struct db_query_arg){ .type = DB_ARG_NULL };
+ case SQLITE_INTEGER:
+ return (struct db_query_arg){ .u.i32 = sqlite3_column_int(stmt, index), .type = DB_ARG_I32 };
+ case SQLITE_FLOAT:
+ return (struct db_query_arg){ .u.f64 = sqlite3_column_double(stmt, index), .type = DB_ARG_F64 };
+ case SQLITE_BLOB:
+ return (struct db_query_arg){ .u.blob = { .data = sqlite3_column_blob(stmt, index), .sz = sqlite3_column_bytes(stmt, index) }, .type = DB_ARG_BLOB };
+ case SQLITE_TEXT:
+ return (struct db_query_arg){ .u.blob = { .data = sqlite3_column_text(stmt, index), .sz = sqlite3_column_bytes(stmt, index) }, .type = DB_ARG_UTF8 };
+ }
+ assert(0 && "should not happen");
+ return (struct db_query_arg){0};
+}
+
+static int
+bind_arg_to_stmt(const struct db_query_arg *arg, const int index, sqlite3_stmt *stmt)
+{
+ assert(arg && stmt);
+ switch (arg->type) {
+ case DB_ARG_NULL:
+ return sqlite3_bind_null(stmt, index);
+ case DB_ARG_I32:
+ return sqlite3_bind_int(stmt, index, arg->u.i32);
+ case DB_ARG_F64:
+ return sqlite3_bind_double(stmt, index, arg->u.f64);
+ case DB_ARG_BLOB:
+ if (arg->u.blob.sz && !arg->u.blob.data)
+ return sqlite3_bind_zeroblob(stmt, index, arg->u.blob.sz);
+ return sqlite3_bind_blob(stmt, index, arg->u.blob.data, arg->u.blob.sz, NULL);
+ case DB_ARG_UTF8:
+ assert(!arg->u.blob.sz || arg->u.blob.data);
+ return sqlite3_bind_text64(stmt, index, arg->u.blob.data, arg->u.blob.sz, NULL, SQLITE_UTF8);
+ }
+ assert(0 && "should not happen");
+ return SQLITE_ERROR;
+}
+
+struct db_query
+db_query_begin(struct db *db, const char *sql, const struct db_query_arg args[], const int num_args)
+{
+ assert(db && sql);
+ assert(args || !num_args);
+
+ sqlite3_stmt *stmt;
+ if (sqlite3_prepare_v2(db->handle, sql, strlen(sql), &stmt, NULL) != SQLITE_OK)
+ errx(EXIT_FAILURE, "sqlite3_prepare_v2: %s", sqlite3_errmsg(db->handle));
+
+ for (int i = 0; i < num_args; ++i) {
+ if (bind_arg_to_stmt(&args[i], i + 1, stmt) != SQLITE_OK)
+ errx(EXIT_FAILURE, "sqlite3_bind: %s", sqlite3_errmsg(db->handle));
+ }
+
+ return (struct db_query){stmt};
+}
+
+int
+db_query_fetch(struct db_query *query, struct db_query_arg columns[], const int num_columns)
+{
+ assert(query);
+ assert(columns || !num_columns);
+
+ int ret;
+ if ((ret = sqlite3_step(query->handle)) != SQLITE_ROW && ret != SQLITE_DONE)
+ errx(EXIT_FAILURE, "sqlite3_step: %s", sqlite3_errstr(ret));
+
+ if (ret == SQLITE_DONE)
+ return 0;
+
+ int count = sqlite3_column_count(query->handle);
+
+ if (!num_columns)
+ return count;
+
+ count = (count > num_columns ? num_columns : count);
+ for (int i = 0; i < count; ++i)
+ columns[i] = stmt_column_to_arg(query->handle, i);
+
+ return count;
+}
+
+void
+db_query_end(struct db_query *query)
+{
+ if (!query)
+ return;
+
+ sqlite3_finalize(query->handle);
+ *query = (struct db_query){0};
+}
+
+int
+db_query_single(struct db *db, const char *sql, const struct db_query_arg args[], const int num_args, struct db_query_arg columns[], const int num_columns)
+{
+ db_query_end(&db->single);
+ db->single = db_query_begin(db, sql, args, num_args);
+ return db_query_fetch(&db->single, columns, num_columns);
+}
+
+void
+db_gc(struct db *db)
+{
+ assert(db);
+ db_query_end(&db->single);
+}
+
+void
+db_release(struct db *db)
+{
+ if (!db)
+ return;
+
+ db_query_end(&db->single);
+ sqlite3_close(db->handle);
+ *db = (struct db){0};
+}
diff --git a/src/db.h b/src/db.h
new file mode 100644
index 0000000..7f01cbf
--- /dev/null
+++ b/src/db.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+struct db_query {
+ void *handle;
+};
+
+struct db {
+ void *handle;
+ struct db_query single;
+};
+
+struct db_query_arg {
+ union {
+ int32_t i32;
+ double f64;
+ struct { const void *data; int sz; } blob; // used for text as well
+ } u;
+ enum {
+ DB_ARG_NULL,
+ DB_ARG_I32,
+ DB_ARG_F64,
+ DB_ARG_BLOB,
+ DB_ARG_UTF8,
+ } type;
+};
+
+void
+db_init(struct db *db, const char *target);
+
+struct db_query
+db_query_begin(struct db *db, const char *sql, const struct db_query_arg args[], const int num_args);
+
+int
+db_query_fetch(struct db_query *query, struct db_query_arg columns[], const int num_columns);
+
+void
+db_query_end(struct db_query *query);
+
+int
+db_query_single(struct db *db, const char *sql, const struct db_query_arg args[], const int num_args, struct db_query_arg columns[], const int num_columns);
+
+void
+db_gc(struct db *db);
+
+void
+db_release(struct db *db);
diff --git a/src/game.c b/src/game.c
new file mode 100644
index 0000000..2337497
--- /dev/null
+++ b/src/game.c
@@ -0,0 +1,629 @@
+#include "packet.h"
+#include "db.h"
+#include "game.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <err.h>
+#include <assert.h>
+
+#include <unistd.h>
+#include <poll.h>
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+#define CENTER(x) (x[0][0] + (x[1][0] - x[0][0]) / 2), (x[0][1] + (x[1][1] - x[0][1]) / 2), (x[0][2] + (x[1][2] - x[0][2]) / 2)
+
+typedef nfds_t client_id;
+
+static struct {
+ struct {
+ char name[13];
+ float pos[3];
+ uint32_t id, zone;
+ uint8_t model, evolution, lvl;
+ } character[1024];
+ uint32_t session_id[1024];
+ struct pollfd pfds[1024];
+ nfds_t npfds;
+ struct { struct db login; } db;
+} server;
+
+enum {
+ FLUSH_TO = 1<<0,
+ FLUSH_ZONE = 1<<1,
+ FLUSH_ALL = 1<<2,
+ FLUSH_EXCLUDE_SENDER = 1<<3,
+};
+
+union flush_arg {
+ client_id to;
+ uint32_t zone;
+};
+
+static void
+flush_packet(client_id sender, union packet *packet, uint32_t filter, union flush_arg arg)
+{
+ packet_crypt(packet);
+
+ if (filter & FLUSH_TO) {
+ (void) !write(server.pfds[arg.to].fd, packet->buf, packet->hdr.size);
+ } else if (filter & FLUSH_ZONE || filter & FLUSH_ALL) {
+ for (nfds_t i = 1; i < server.npfds; ++i) {
+ if ((filter & FLUSH_EXCLUDE_SENDER) && i == sender)
+ continue;
+ if ((filter & FLUSH_ZONE) && server.character[sender].zone != server.character[i].zone)
+ continue;
+ (void) !write(server.pfds[i].fd, packet->buf, packet->hdr.size);
+ }
+ }
+}
+
+static void
+stub(client_id cid, union packet *packet, uint16_t type, uint8_t byte)
+{
+ packet->hdr.type = type;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ for (size_t i = 0; i < 1024; ++i)
+ pbuf_write(&pbuf, (uint8_t[]){byte}, 1); // unknown
+ pbuf_flush(&pbuf);
+ warnx("sending stub 0x%.4x", type);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+load_shop_banners(client_id cid, union packet *packet, const char *urls[], size_t num_urls)
+{
+ packet->hdr.type = PACKET_SHOP_BANNER_LOAD;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint32_t[]){num_urls}, sizeof(uint32_t)); // num banners
+ for (size_t i = 0; i < num_urls; ++i) {
+ pbuf_write_str(&pbuf, urls[i]); // url
+ pbuf_write(&pbuf, (uint32_t[]){0x00}, sizeof(uint32_t)); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){0x00}, sizeof(uint32_t)); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){0x00}, sizeof(uint32_t)); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){0x00}, sizeof(uint32_t)); // unknown
+ }
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+load_pcs_in_zone(client_id cid, union packet *packet, uint32_t zone)
+{
+ packet->hdr.type = PACKET_OTHER_PC_INFOS;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint16_t[]){0x00}, sizeof(uint16_t)); // num pcs (placeholder);
+
+ for (uint16_t i = 1; i < server.npfds; ++i) {
+ if (i == cid || server.character[cid].zone != zone)
+ continue;
+
+ pbuf_write(&pbuf, &server.character[i].id, sizeof(uint32_t)); // charid
+ pbuf_write_str_utf16(&pbuf, server.character[i].name); // char name UTF16
+ pbuf_write(&pbuf, &server.character[i].model, 1); // char model (lily, haru, ...), 0 for no char
+ pbuf_write(&pbuf, &server.character[i].evolution, 1); // evolution lvl
+ for (size_t i = 0; i < 20; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // skin/eye/body/etc
+ pbuf_write(&pbuf, &server.character[i].lvl, 1); // char lvl
+ for (size_t i = 0; i < 452; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // fashion?
+ pbuf_write_str_len(&pbuf, NULL, 0); // guild name
+ pbuf_write(&pbuf, (uint32_t[]){0x00}, sizeof(uint32_t)); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){155, 155}, sizeof(uint32_t) * 2); // hp, max hp
+ pbuf_write(&pbuf, (uint32_t[]){133, 133}, sizeof(uint32_t) * 2); // sf, max sf
+ for (size_t i = 0; i < 8; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){300, 400}, sizeof(uint32_t) * 2); // unknown
+ for (size_t i = 0; i < 10; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){0x42f0, 0x42d2}, sizeof(uint32_t) * 2); // movement speed?
+ for (size_t i = 0; i < 19; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, &zone, sizeof(zone)); // zone
+ for (size_t i = 0; i < 6; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, server.character[i].pos, sizeof(float) * 3); // xyz
+ for (size_t i = 0; i < 76; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // prologue?
+ pbuf_write(&pbuf, (uint8_t[]){0x01}, 1); // unknown
+ memcpy(packet->buf + sizeof(packet->hdr), &i, sizeof(uint16_t));
+ }
+
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+send_pc_info(client_id cid, union packet *packet, bool to_self)
+{
+ packet->hdr.type = (to_self ? PACKET_CHARACTER_INFO_RES : PACKET_IN_PC_INFO);
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, &server.character[cid].id, sizeof(uint32_t)); // charid
+ pbuf_write_str_utf16(&pbuf, server.character[cid].name); // char name UTF16
+ pbuf_write(&pbuf, &server.character[cid].model, 1); // char model (lily, haru, ...), 0 for no char
+ pbuf_write(&pbuf, &server.character[cid].evolution, 1); // evolution lvl
+ for (size_t i = 0; i < 20; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // skin/eye/body/etc
+ pbuf_write(&pbuf, &server.character[cid].lvl, 1); // char lvl
+ for (size_t i = 0; i < 452; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // fashion?
+ pbuf_write_str_len(&pbuf, NULL, 0); // guild name
+ pbuf_write(&pbuf, (uint32_t[]){0x00}, sizeof(uint32_t)); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){155, 155}, sizeof(uint32_t) * 2); // hp, max hp
+ pbuf_write(&pbuf, (uint32_t[]){133, 133}, sizeof(uint32_t) * 2); // sf, max sf
+ for (size_t i = 0; i < 8; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){300, 400}, sizeof(uint32_t) * 2); // unknown
+ for (size_t i = 0; i < 10; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (uint32_t[]){0x42f0, 0x42d2}, sizeof(uint32_t) * 2); // movement speed?
+ for (size_t i = 0; i < 19; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, &server.character[cid].zone, sizeof(uint32_t)); // zone
+ for (size_t i = 0; i < 6; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, server.character[cid].pos, sizeof(float) * 3); // xyz
+ for (size_t i = 0; i < 76; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // prologue?
+ pbuf_write(&pbuf, (uint8_t[]){0x01}, 1); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, (to_self ? FLUSH_TO : FLUSH_ZONE | FLUSH_EXCLUDE_SENDER), (union flush_arg){ .to = cid });
+}
+
+static void
+send_despawn(client_id cid, union packet *packet)
+{
+ packet->hdr.type = PACKET_OUT_INFO_PC;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, &server.character[cid].id, sizeof(uint32_t)); // charid
+ pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_ZONE | FLUSH_EXCLUDE_SENDER, (union flush_arg){0});
+}
+
+static void
+spawn_to(client_id cid, union packet *packet, uint32_t zone, float x, float y, float z)
+{
+ warnx("spawning to: 0x%.4x", zone);
+
+ send_despawn(cid, packet);
+ server.character[cid].zone = zone;
+ server.character[cid].pos[0] = x;
+ server.character[cid].pos[1] = y;
+ server.character[cid].pos[2] = z;
+ send_pc_info(cid, packet, true);
+
+ packet->hdr.type = PACKET_SKILLS;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ for (size_t i = 0; i < 232; ++i) pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+
+}
+
+static void
+zone_to(client_id cid, union packet *packet, uint32_t zone, float x, float y, float z)
+{
+ warnx("zoning to: 0x%.4x", zone);
+
+ packet->hdr.type = PACKET_GAME_WORLD_ENTER_RES;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, &server.character[cid].id, sizeof(uint32_t)); // charid
+ for (size_t i = 0; i < 20; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, &zone, sizeof(zone)); // zone
+ for (size_t i = 0; i < 8; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+
+ const char *host = getenv("TCPREMOTEIP");
+ if (!host || strcmp(host, "127.0.0.1")) {
+ host = getenv("GAME_SERVER");
+ host = (host ? host : "84.248.5.167");
+ }
+
+ pbuf_write_str(&pbuf, host); // server ip
+ pbuf_write(&pbuf, (uint16_t[]){10100}, sizeof(uint16_t)); // port
+ pbuf_write(&pbuf, (uint8_t[]){0xff, 0xff}, 2); // unknown
+ for (size_t i = 0; i < 8; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (float[]){x, y, z}, sizeof(float) * 3); // xyz
+ for (size_t i = 0; i < 17; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_write(&pbuf, (uint8_t[]){0x01}, sizeof(uint8_t)); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+
+ send_despawn(cid, packet);
+ server.character[cid].zone = zone;
+ server.character[cid].pos[0] = x;
+ server.character[cid].pos[1] = y;
+ server.character[cid].pos[2] = z;
+}
+
+static void
+send_channel_info(client_id cid, union packet *packet)
+{
+ packet->hdr.type = PACKET_CHANNEL_INFO;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint16_t[]){server.character[cid].zone}, sizeof(uint16_t)); // zone
+
+ uint8_t num_channels = 1;
+ pbuf_write(&pbuf, &num_channels, sizeof(uint8_t)); // number of channels
+ for (uint16_t i = 1; i <= num_channels; ++i) {
+ pbuf_write(&pbuf, &i, sizeof(uint16_t)); // channel index (dunno why 16bit)
+ pbuf_write(&pbuf, (uint8_t[]){0}, 1); // bar (0..3)
+ }
+
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+handle_character_action_req(client_id cid, union packet *packet)
+{
+ packet->hdr.type = PACKET_CHARACTER_ACTION_RES;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+#if 0
+ const uint8_t buf[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x65,
+ 0xeb, 0xa9, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xe0, 0x03, 0xdc, 0x19, 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0xdc, 0x19,
+ 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+ };
+ pbuf_write(&pbuf, buf, sizeof(buf));
+#endif
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+handle_shop_cash_load(client_id cid, union packet *packet)
+{
+ packet->hdr.type = PACKET_SHOP_CASH_LOAD;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint8_t[]){0x01, 0x00, 0x00, 0x00, 0x00}, 5); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+handle_event_use_coupon_code(client_id cid, union packet *packet)
+{
+ // string
+}
+
+static void
+handle_maze_create_req(client_id cid, union packet *packet)
+{
+ uint32_t zone;
+ memcpy(&zone, packet->buf + 20, sizeof(zone));
+
+ for (size_t i = 0; i < ARRAY_SIZE(ZONES); ++i) {
+ if (ZONES[i].id != zone)
+ continue;
+
+ union packet packet = { .hdr.salt = 2 };
+ zone_to(cid, &packet, ZONES[i].id, CENTER(ZONES[i].box));
+ break;
+ }
+}
+
+static void
+handle_character_info_req(client_id cid, union packet *packet)
+{
+ spawn_to(cid, packet, 10003, 10230, 10058, 90);
+}
+
+static void
+load_cash_shop(client_id cid, union packet *packet)
+{
+ load_shop_banners(cid, packet, (const char*[]){"https://cloudef.pw/armpit/sw-encryption.png"}, 1);
+ handle_shop_cash_load(cid, packet);
+
+ packet->hdr.type = PACKET_SHOP_CASH_TAB_LOAD;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint8_t[]){
+ 0x07, 0x00, 0x00, 0x00, 0xf2, 0x03, 0x00, 0x00, 0x01, 0x00, 0x0a, 0x00,
+ 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x02, 0x00, 0x0a, 0x00, 0x00, 0x00,
+ 0xfd, 0x03, 0x00, 0x00, 0x01, 0x00, 0xfe, 0x03, 0x00, 0x00, 0x02, 0x00,
+ 0xff, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00,
+ 0x01, 0x04, 0x00, 0x00, 0x05, 0x00, 0x02, 0x04, 0x00, 0x00, 0x06, 0x00,
+ 0x03, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x06, 0x04, 0x00, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x07, 0x04,
+ 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x04,
+ 0x00, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00,
+ 0x01, 0x00, 0x12, 0x04, 0x00, 0x00, 0x02, 0x00, 0x13, 0x04, 0x00, 0x00,
+ 0x03, 0x00, 0x14, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x04, 0x00, 0x00,
+ 0x05, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x1b, 0x04, 0x00, 0x00, 0x01, 0x00,
+ 0x1c, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x04, 0x00, 0x00, 0x06, 0x00,
+ 0x0a, 0x00, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, 0x01, 0x00, 0x26, 0x04,
+ 0x00, 0x00, 0x02, 0x00, 0x27, 0x04, 0x00, 0x00, 0x03, 0x00, 0x28, 0x04,
+ 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, 0x07, 0x00, 0x0a, 0x00,
+ 0x00, 0x00, 0x2f, 0x04, 0x00, 0x00, 0x01, 0x00, 0x30, 0x04, 0x00, 0x00,
+ 0x02, 0x00, 0x31, 0x04, 0x00, 0x00, 0x03, 0x00, 0x32, 0x04, 0x00, 0x00,
+ 0x04, 0x00, 0x33, 0x04, 0x00, 0x00, 0x05, 0x00, 0x34, 0x04, 0x00, 0x00,
+ 0x06, 0x00, 0x35, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00
+ }, 494);
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+
+ packet->hdr.type = PACKET_SHOP_CASH_SET_LOAD;
+ pbuf = (struct pbuf){ .packet = packet, .cursor = sizeof(packet->hdr) };
+ for (size_t i = 0; i < 749; ++i) pbuf_write(&pbuf, (uint8_t[]){0}, 1);
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+
+ packet->hdr.type = PACKET_SHOP_CASH_BUY_COUNT_LOAD;
+ pbuf = (struct pbuf){ .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint8_t[]){0x00, 0x00, 0x00, 0x00}, 4);
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+handle_enter_gameserver_req(client_id cid, union packet *packet)
+{
+ {
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_read(&pbuf, &server.session_id[cid], sizeof(uint32_t));
+ }
+
+ struct db_query_arg args[] = {{ .type = DB_ARG_I32, .u.i32 = server.session_id[cid] }}, cols[5];
+ const int results = db_query_single(&server.db.login,
+ "select id, model, name, evolution, lvl from characters where id = (select char_id from sessions where id = ? limit 1) order by id",
+ args, ARRAY_SIZE(args), cols, ARRAY_SIZE(cols));
+
+ assert(results == ARRAY_SIZE(cols));
+ assert(cols[0].type == DB_ARG_I32 && cols[1].type == DB_ARG_I32 && cols[3].type == DB_ARG_I32 && cols[4].type == DB_ARG_I32);
+ assert(cols[2].type == DB_ARG_UTF8 && (size_t)cols[2].u.blob.sz < sizeof(server.character[cid].name));
+
+ server.character[cid].id = cols[0].u.i32;
+ server.character[cid].model = cols[1].u.i32;
+ memcpy(server.character[cid].name, cols[2].u.blob.data, cols[2].u.blob.sz);
+ server.character[cid].evolution = cols[3].u.i32;
+ server.character[cid].lvl = cols[4].u.i32;
+
+ // stub(cid, packet, PACKET_AUTH_UNKNOWN1, 0x01);
+ // stub(cid, packet, PACKET_WORLD_VERSION, 0x01);
+ // stub(cid, packet, PACKET_AUTH_UNKNOWN17, 0x01);
+
+ stub(cid, packet, PACKET_ENTER_GAMESERVER_RES, 0x01);
+
+ // stub(cid, packet, PACKET_GAME_UNKNOWN1, 0x01);
+ // stub(cid, packet, PACKET_GAME_UNKNOWN2, 0x01);
+ // stub(cid, packet, PACKET_GAME_UNKNOWN3, 0x01);
+
+ stub(cid, packet, PACKET_CHARACTER_DB_LOAD_SYNC, 0x01);
+
+ // load_cash_shop(cid, packet);
+}
+
+static void
+inject(client_id cid, const char *arg)
+{
+ warnx("injecting: %s", arg);
+
+ FILE *f;
+ if (!(f = fopen(arg, "rb"))) {
+ warn("fopen(%s)", arg);
+ return;
+ }
+
+ union packet packet;
+ (void) !fread(packet.buf, 1, sizeof(packet.buf), f);
+ fclose(f);
+
+ packet_crypt(&packet);
+ flush_packet(cid, &packet, FLUSH_TO, (union flush_arg){ .to = cid });
+}
+
+static void
+zone(client_id cid, const char *arg)
+{
+ warnx("zone: %s", arg);
+
+ for (size_t i = 0; i < ARRAY_SIZE(ZONES); ++i) {
+ if (strcmp(ZONES[i].name, arg))
+ continue;
+
+ union packet packet = { .hdr.salt = 2 };
+ zone_to(cid, &packet, ZONES[i].id, CENTER(ZONES[i].box));
+ break;
+ }
+}
+
+static char *const *ARGV;
+
+static void
+rexec(void)
+{
+ warnx("running exec");
+ execv(ARGV[0], ARGV);
+}
+
+static void
+handle_chat_normal(client_id cid, union packet *packet)
+{
+ uint8_t unknown;
+ struct pbuf_string msg;
+ {
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_read(&pbuf, &unknown, sizeof(unknown)); // unknown (type of msg?)
+ pbuf_read_str_utf16(&pbuf, &msg);
+ }
+
+ const struct {
+ const char *cmd;
+ void (*action)();
+ } map[] = {
+ { .cmd = "ex", .action = rexec },
+ { .cmd = "inject", .action = inject },
+ { .cmd = "zone", .action = zone },
+ };
+
+ for (size_t i = 0; i < ARRAY_SIZE(map); ++i) {
+ if (strncmp(msg.data, map[i].cmd, strlen(map[i].cmd)))
+ continue;
+
+ map[i].action(cid, msg.data + strlen(map[i].cmd) + 1);
+ return;
+ }
+
+ packet->hdr.type = PACKET_CHAT_NORMAL;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, &server.character[cid].id, sizeof(uint32_t)); // charid
+ pbuf_write(&pbuf, &unknown, 1); // msg type?
+ pbuf_write(&pbuf, (uint8_t[]){0, 0, 0}, 3); // unknown
+ pbuf_write_str_len_utf16(&pbuf, msg.data, msg.len); // msg text UTF16
+ pbuf_write(&pbuf, (uint8_t[]){0}, 1); // null terminate
+ pbuf_flush(&pbuf);
+ flush_packet(cid, packet, FLUSH_ALL, (union flush_arg){0});
+}
+
+static void
+handle_packet(client_id cid, union packet *packet)
+{
+ packet_crypt(packet);
+ warnx("got packet of type 0x%.4x", packet->hdr.type);
+
+ switch (packet->hdr.type) {
+ case PACKET_ENTER_GAMESERVER_REQ:
+ handle_enter_gameserver_req(cid, packet);
+ break;
+ case PACKET_CHARACTER_INFO_REQ:
+ handle_character_info_req(cid, packet);
+ break;
+ case PACKET_CHANNEL_INFO:
+ send_channel_info(cid, packet);
+ /* fallthrough */
+ case PACKET_COMPLETE_MAZE_REQ:
+ load_pcs_in_zone(cid, packet, server.character[cid].zone);
+ send_pc_info(cid, packet, false);
+ break;
+ case PACKET_CHARACTER_MOVE:
+ packet->hdr.type = PACKET_CHARACTER_MOVE_RES;
+ flush_packet(cid, packet, FLUSH_ZONE | FLUSH_EXCLUDE_SENDER, (union flush_arg){0});
+ break;
+ case PACKET_CHARACTER_STOP_MOVE:
+ packet->hdr.type = PACKET_CHARACTER_STOP_MOVE_RES;
+ flush_packet(cid, packet, FLUSH_ZONE | FLUSH_EXCLUDE_SENDER, (union flush_arg){0});
+ break;
+ case PACKET_CHARACTER_JUMP:
+ packet->hdr.type = PACKET_CHARACTER_JUMP_RES;
+ flush_packet(cid, packet, FLUSH_ZONE | FLUSH_EXCLUDE_SENDER, (union flush_arg){0});
+ break;
+ case PACKET_CHAT_NORMAL:
+ handle_chat_normal(cid, packet);
+ break;
+ case PACKET_MAZE_CREATE_REQ:
+ handle_maze_create_req(cid, packet);
+ break;
+ case PACKET_EVENT_USE_COUPON_CODE:
+ handle_event_use_coupon_code(cid, packet);
+ break;
+ case PACKET_SHOP_CASH_LOAD:
+ handle_shop_cash_load(cid, packet);
+ break;
+ case PACKET_CHARACTER_ACTION_REQ:
+ handle_character_action_req(cid, packet);
+ break;
+ default:
+ warnx("unknown packet");
+ break;
+ }
+}
+
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <fcntl.h>
+
+static inline size_t
+safe_read(int fd, void *dst, const size_t dst_size)
+{
+ size_t off = 0;
+ ssize_t ret = 0;
+ for (; off < dst_size && (ret = read(fd, (char*)dst + off, dst_size - off)) > 0; off += ret);
+ return (ret < 0 ? 0 : off);
+}
+
+int
+main(int argc, char *const argv[])
+{
+ (void)argc;
+ ARGV = argv;
+
+ db_init(&server.db.login, "login.db");
+
+ int sfd;
+ if ((sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
+ err(EXIT_FAILURE, "socket");
+
+ struct sockaddr_in serv_addr = {
+ .sin_family = AF_INET,
+ .sin_addr.s_addr = htonl(INADDR_ANY),
+ .sin_port = htons(10100),
+ };
+
+ setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, (int[]){true}, sizeof(int));
+
+ if (bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) != 0)
+ err(EXIT_FAILURE, "bind");
+
+ if (listen(sfd, 0) != 0)
+ err(EXIT_FAILURE, "listen");
+
+ union packet packet;
+ server.pfds[0] = (struct pollfd){ .fd = sfd, .events = POLLIN };
+ server.npfds = 1;
+ while (poll(server.pfds, server.npfds, -1) > 0) {
+ if (server.pfds[0].revents & POLLIN) {
+ int cfd;
+ if ((cfd = accept(sfd, (struct sockaddr*)NULL, NULL)) == -1) {
+ warnx("failed to accept");
+ continue;
+ }
+
+ if (setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, (int[]){true}, sizeof(int)) != 0)
+ err(EXIT_FAILURE, "setsockopt");
+
+ if (fcntl(cfd, F_SETFL, fcntl(cfd, F_GETFL, 0) | O_NONBLOCK) == -1)
+ err(EXIT_FAILURE, "failed to set O_NONBLOCK");
+
+ server.pfds[server.npfds].fd = cfd;
+ server.pfds[server.npfds++].events = POLLIN;
+ warnx("new client connected");
+ }
+
+ for (nfds_t i = 1; i < server.npfds; ++i) {
+ if (!(server.pfds[i].revents & POLLIN))
+ continue;
+
+ if (safe_read(server.pfds[i].fd, packet.buf, sizeof(packet.hdr)) != sizeof(packet.hdr) || !packet_verify(&packet)) {
+ warnx("invalid packet");
+ close(server.pfds[i].fd);
+ memmove(&server.pfds[i], &server.pfds[i + 1], sizeof(struct pollfd) * server.npfds - i);
+ memmove(&server.session_id[i], &server.session_id[i + 1], sizeof(uint32_t) * server.npfds - i);
+ memmove(&server.character[i], &server.character[i + 1], sizeof(*server.character) * server.npfds - i);
+ --server.npfds;
+ continue;
+ }
+
+ safe_read(server.pfds[i].fd, packet.buf + sizeof(packet.hdr), packet.hdr.size - sizeof(packet.hdr));
+ handle_packet(i, &packet);
+ }
+
+ db_gc(&server.db.login);
+ }
+
+ db_release(&server.db.login);
+ return EXIT_SUCCESS;
+}
diff --git a/src/game.h b/src/game.h
new file mode 100644
index 0000000..5a18cdb
--- /dev/null
+++ b/src/game.h
@@ -0,0 +1,195 @@
+#pragma once
+
+static const struct {
+ float box[2][3];
+ const char *name;
+ uint32_t id;
+} ZONES[] = {
+ { .id = 21100, .name = "ABANDONEDGRAVE", .box = { { 71765.359,42776.707,11445.727 }, { 71965.359,42976.707,11545.727 } } },
+ { .id = 24611, .name = "ACHERON_EP_01", .box = { { 1624.729,3726.533,4000.000 }, { 1824.729,3926.533,4100.000 } } },
+ { .id = 24621, .name = "ACHERON_EP_02", .box = { { 5730.963,5195.774,4000.000 }, { 5930.963,5395.774,4100.000 } } },
+ { .id = 24631, .name = "ACHERON_EP_03", .box = { { 755.223,4541.032,4000.000 }, { 955.223,4741.032,4100.000 } } },
+ { .id = 24641, .name = "ACHERON_EP_04", .box = { { 4373.032,9195.787,4002.171 }, { 4573.032,9395.787,4102.171 } } },
+ { .id = 21015, .name = "ALTAROFVOID", .box = { { 32667.709,42776.703,12349.053 }, { 32867.711,42976.703,12449.053 } } },
+ { .id = 23015, .name = "ALTAROFVOID_RANK", .box = { { 71611.805,42767.980,11445.727 }, { 71811.805,42967.980,11545.727 } } },
+ { .id = 23611, .name = "AREA9_EP_01", .box = { { 45635.855,59211.266,12818.359 }, { 45735.855,59311.266,12918.359 } } },
+ { .id = 23621, .name = "AREA9_EP_02", .box = { { 14942.157,39259.754,7475.312 }, { 15042.157,39359.754,7575.312 } } },
+ { .id = 23631, .name = "AREA9_EP_03", .box = { { 14942.157,39259.754,7475.312 }, { 15042.157,39359.754,7575.312 } } },
+ { .id = 23641, .name = "AREA9_EP_04", .box = { { 96694.180,21313.873,7475.312 }, { 96794.180,21413.873,7575.312 } } },
+ { .id = 24711, .name = "ARKSHIP_EP_01", .box = { { 21512.742,48088.230,32086.607 }, { 21612.742,48188.230,32186.607 } } },
+ { .id = 24721, .name = "ARKSHIP_EP_02", .box = { { 19919.807,48580.500,33337.152 }, { 20019.807,48680.500,33437.152 } } },
+ { .id = 24731, .name = "ARKSHIP_EP_03", .box = { { 24215.566,53405.254,33760.469 }, { 24315.566,53505.254,33860.469 } } },
+ { .id = 24741, .name = "ARKSHIP_EP_04", .box = { { 8349.707,47541.566,33755.699 }, { 8449.707,47641.566,33855.699 } } },
+ { .id = 21013, .name = "BANDITSHIGHWAY", .box = { { 19029.584,86538.711,9137.494 }, { 19129.584,86638.711,9237.494 } } },
+ { .id = 21013, .name = "BANDITSHIGHWAY_BLUE", .box = { { 19029.584,86538.711,9137.494 }, { 19129.584,86638.711,9237.494 } } },
+ { .id = 21013, .name = "BANDITSHIGHWAY_DARK", .box = { { 19029.584,86538.711,9137.494 }, { 19129.584,86638.711,9237.494 } } },
+ { .id = 21013, .name = "BANDITSHIGHWAY_GREEN", .box = { { 19029.584,86538.711,9137.494 }, { 19129.584,86638.711,9237.494 } } },
+ { .id = 21013, .name = "BANDITSHIGHWAY_LIGHT", .box = { { 19029.584,86538.711,9137.494 }, { 19129.584,86638.711,9237.494 } } },
+ { .id = 21013, .name = "BANDITSHIGHWAY_RED", .box = { { 19029.584,86538.711,9137.494 }, { 19129.584,86638.711,9237.494 } } },
+ { .id = 21013, .name = "BANDITSHIGHWAY_WHITE", .box = { { 19029.584,86538.711,9137.494 }, { 19129.584,86638.711,9237.494 } } },
+ { .id = 21001, .name = "BATTLECOURT", .box = { { 9094.386,6584.809,2525.728 }, { 10294.386,7184.809,2825.728 } } },
+ { .id = 21321, .name = "BESTSHOWTIME_EP_01", .box = { { 20309.111,13561.008,255.900 }, { 20409.111,13661.008,355.900 } } },
+ { .id = 21421, .name = "BESTSHOWTIME_EP_02", .box = { { 6560.845,28540.777,4810.046 }, { 6660.845,28640.777,4910.046 } } },
+ { .id = 21431, .name = "BESTSHOWTIME_EP_03", .box = { { 22493.889,11822.354,242.868 }, { 22593.889,11922.354,342.868 } } },
+ { .id = 21431, .name = "BESTSHOWTIME_EP_04", .box = { { 22421.064,11822.354,242.868 }, { 22521.064,11922.354,342.868 } } },
+ { .id = 26311, .name = "BETRAYERSARMY_EP_01", .box = { { 25004.813,65614.461,49524.094 }, { 25104.813,65714.461,49624.094 } } },
+ { .id = 26321, .name = "BETRAYERSARMY_EP_02", .box = { { 23016.813,65614.461,49524.094 }, { 23116.813,65714.461,49624.094 } } },
+ { .id = 26331, .name = "BETRAYERSARMY_EP_03", .box = { { 14449.455,59277.570,49524.094 }, { 14549.455,59377.570,49624.094 } } },
+ { .id = 26341, .name = "BETRAYERSARMY_EP_04", .box = { { 17666.430,61969.102,49524.094 }, { 17766.430,62069.102,49624.094 } } },
+ { .id = 22511, .name = "BREAKOUT_EP_01", .box = { { 19948.703,25796.875,1573.456 }, { 20048.703,25896.875,1673.456 } } },
+ { .id = 22521, .name = "BREAKOUT_EP_02", .box = { { 48158.621,37457.180,1879.832 }, { 48258.621,37557.180,1979.832 } } },
+ { .id = 22531, .name = "BREAKOUT_EP_03", .box = { { 18132.455,35004.230,1254.304 }, { 18232.455,35104.230,1354.304 } } },
+ { .id = 22541, .name = "BREAKOUT_EP_04", .box = { { 6282.323,85145.914,5692.782 }, { 6382.323,85245.914,5792.782 } } },
+ { .id = 24211, .name = "BUSTERCORE_EP_01", .box = { { 10492.922,13735.813,8956.066 }, { 10592.922,13835.813,9056.066 } } },
+ { .id = 24221, .name = "BUSTERCORE_EP_02", .box = { { 43661.035,54645.117,9911.659 }, { 43761.035,54745.117,10011.659 } } },
+ { .id = 24231, .name = "BUSTERCORE_EP_03", .box = { { 43656.527,54700.977,9908.667 }, { 43756.527,54800.977,10008.667 } } },
+ { .id = 24241, .name = "BUSTERCORE_EP_04", .box = { { 47319.137,38907.820,9902.705 }, { 47419.137,39007.820,10002.705 } } },
+ { .id = 10021, .name = "CANDUSCITY", .box = { { 79697.867,75670.805,1716.357 }, { 80497.867,76170.805,2016.357 } } },
+ { .id = 23411, .name = "COLDRAIN_EP_01", .box = { { 8901.713,37108.027,1000.000 }, { 9101.713,37308.027,1100.000 } } },
+ { .id = 23421, .name = "COLDRAIN_EP_02", .box = { { 10396.657,47618.223,999.999 }, { 10596.657,47818.223,1099.999 } } },
+ { .id = 23431, .name = "COLDRAIN_EP_03", .box = { { 13391.982,64837.066,1000.000 }, { 13591.982,65037.066,1100.000 } } },
+ { .id = 23441, .name = "COLDRAIN_EP_04", .box = { { 8899.502,73578.008,1000.000 }, { 9099.502,73778.008,1100.000 } } },
+ { .id = 24311, .name = "COLDREVENGE_EP_01", .box = { { 4503.840,8090.859,1000.003 }, { 4603.840,8190.859,1100.003 } } },
+ { .id = 24321, .name = "COLDREVENGE_EP_02", .box = { { 5997.292,9230.892,1000.713 }, { 6097.292,9330.892,1100.713 } } },
+ { .id = 24331, .name = "COLDREVENGE_EP_03", .box = { { 1544.485,8084.674,1000.000 }, { 1644.485,8184.674,1100.000 } } },
+ { .id = 24341, .name = "COLDREVENGE_EP_04", .box = { { 23842.326,22021.086,1000.000 }, { 23942.326,22121.086,1100.000 } } },
+ { .id = 22111, .name = "CONCRETEJUNGLE_EP_01", .box = { { 15219.798,16879.947,5846.374 }, { 15319.798,16979.947,5946.374 } } },
+ { .id = 22121, .name = "CONCRETEJUNGLE_EP_02", .box = { { 27579.756,46421.961,5847.986 }, { 27679.756,46521.961,5947.986 } } },
+ { .id = 22131, .name = "CONCRETEJUNGLE_EP_03", .box = { { 30245.184,14571.286,7174.145 }, { 30345.184,14671.286,7274.145 } } },
+ { .id = 22141, .name = "CONCRETEJUNGLE_EP_04", .box = { { 88305.086,30384.449,5857.868 }, { 88405.086,30484.449,5957.868 } } },
+ { .id = 24111, .name = "CONTROLBASE_EP_01", .box = { { 2192.828,4900.000,100.000 }, { 2392.828,5100.000,200.000 } } },
+ { .id = 24121, .name = "CONTROLBASE_EP_02", .box = { { 20117.607,34891.281,1800.000 }, { 20317.607,35091.281,1900.000 } } },
+ { .id = 24131, .name = "CONTROLBASE_EP_03", .box = { { 11036.818,29912.539,100.000 }, { 11236.818,30112.539,200.000 } } },
+ { .id = 24141, .name = "CONTROLBASE_EP_04", .box = { { 13107.208,18393.436,102.000 }, { 13307.208,18593.436,202.000 } } },
+ { .id = 21211, .name = "CONTROLZONE43_EP01", .box = { { 9062.172,25195.049,10.000 }, { 9162.172,25295.049,110.000 } } },
+ { .id = 21221, .name = "CONTROLZONE43_EP02", .box = { { 18912.811,20551.725,10.000 }, { 19012.811,20651.725,110.000 } } },
+ { .id = 21231, .name = "CONTROLZONE43_EP03", .box = { { 9062.172,25195.049,85.824 }, { 9162.172,25295.049,185.824 } } },
+ { .id = 21241, .name = "CONTROLZONE43_EP04", .box = { { 9062.172,25195.049,10.000 }, { 9162.172,25295.049,110.000 } } },
+ { .id = 25511, .name = "CORRUPTEDRECORD_EP_01", .box = { { 9512.718,29377.615,8458.003 }, { 9612.718,29477.615,8558.003 } } },
+ { .id = 25521, .name = "CORRUPTEDRECORD_EP_02", .box = { { 6385.772,27405.297,7528.966 }, { 6485.772,27505.297,7628.966 } } },
+ { .id = 25531, .name = "CORRUPTEDRECORD_EP_03", .box = { { 15489.951,23631.916,4587.734 }, { 15589.951,23731.916,4687.734 } } },
+ { .id = 25541, .name = "CORRUPTEDRECORD_EP_04", .box = { { 10602.390,13711.694,6064.722 }, { 10702.390,13811.694,6164.722 } } },
+ { .id = 25541, .name = "CORRUPTEDRECORD_EP_05", .box = { { 60908.660,13351.867,5204.879 }, { 61008.660,13451.867,5304.879 } } },
+ { .id = 25411, .name = "DEADMEATFACTORY_EP_01", .box = { { 9030.938,25519.398,9058.996 }, { 9130.938,25619.398,9158.996 } } },
+ { .id = 25421, .name = "DEADMEATFACTORY_EP_02", .box = { { 10115.220,21198.301,7870.886 }, { 10215.220,21298.301,7970.886 } } },
+ { .id = 25431, .name = "DEADMEATFACTORY_EP_03", .box = { { 20989.686,16702.158,7850.639 }, { 21089.686,16802.158,7950.639 } } },
+ { .id = 25441, .name = "DEADMEATFACTORY_EP_04", .box = { { 26273.787,21810.379,7933.864 }, { 26373.787,21910.379,8033.864 } } },
+ { .id = 24511, .name = "DEEPCORE_EP_01", .box = { { 7948.050,31228.832,37232.531 }, { 8148.050,31428.832,37332.531 } } },
+ { .id = 24521, .name = "DEEPCORE_EP_02", .box = { { 33796.648,22982.727,36583.289 }, { 33996.648,23182.727,36683.289 } } },
+ { .id = 24531, .name = "DEEPCORE_EP_03", .box = { { 23454.346,28538.414,36953.910 }, { 23654.346,28738.414,37053.910 } } },
+ { .id = 24541, .name = "DEEPCORE_EP_04", .box = { { 6059.498,28585.688,37236.887 }, { 6259.498,28785.688,37336.887 } } },
+ { .id = 11001, .name = "DF01_GOLDENCITADEL", .box = { { 18630.219,17783.496,4563.968 }, { 19030.219,18183.496,4663.968 } } },
+ { .id = 21101, .name = "DIMENTIONSHUTER", .box = { { 18597.697,20098.609,11448.418 }, { 18697.697,20198.609,11548.418 } } },
+ { .id = 10061, .name = "DIPLUCEHORIZON", .box = { { 53620.297,43903.453,28561.432 }, { 54020.297,44303.453,28661.432 } } },
+ { .id = 30021, .name = "DISTRICT6", .box = { { 42440.641,69162.828,15509.875 }, { 43240.641,69962.828,15709.875 } } },
+ { .id = 30021, .name = "DISTRICT6_THETHING", .box = { { 42440.641,69162.828,15509.875 }, { 43240.641,69962.828,15709.875 } } },
+ { .id = 21051, .name = "DR01_GOLDENCITADEL", .box = { { 18630.664,17167.801,4917.395 }, { 19130.664,17667.801,5017.395 } } },
+ { .id = 25111, .name = "DREADFULECHO_EP_01", .box = { { 18156.283,18184.186,103.000 }, { 18256.283,18284.186,203.000 } } },
+ { .id = 25121, .name = "DREADFULECHO_EP_02", .box = { { 10753.356,28643.342,100.000 }, { 10853.356,28743.342,200.000 } } },
+ { .id = 25131, .name = "DREADFULECHO_EP_03", .box = { { 32069.379,24957.131,7174.459 }, { 32169.379,25057.131,7274.459 } } },
+ { .id = 25131, .name = "DREADFULECHO_EP_04", .box = { { 32497.932,50652.387,7123.774 }, { 32597.932,50752.387,7223.774 } } },
+ { .id = 23511, .name = "FLAMEBREAKER_EP_01", .box = { { 7976.539,18972.150,10000.000 }, { 8076.539,19072.150,10100.000 } } },
+ { .id = 23521, .name = "FLAMEBREAKER_EP_02", .box = { { 21035.801,37906.941,10019.125 }, { 21135.801,38006.941,10119.125 } } },
+ { .id = 23531, .name = "FLAMEBREAKER_EP_03", .box = { { 12187.206,16561.766,9299.523 }, { 12287.206,16661.766,9399.523 } } },
+ { .id = 23541, .name = "FLAMEBREAKER_EP_04", .box = { { 19837.080,46038.531,9117.851 }, { 19937.080,46138.531,9217.851 } } },
+ { .id = 24411, .name = "FORGOTTENARMORY_EP_01", .box = { { 1629.080,35858.887,41931.645 }, { 1729.080,35958.887,42031.645 } } },
+ { .id = 24421, .name = "FORGOTTENARMORY_EP_02", .box = { { 8336.573,35812.203,41931.645 }, { 8436.573,35912.203,42031.645 } } },
+ { .id = 24431, .name = "FORGOTTENARMORY_EP_03", .box = { { 51145.582,16388.814,43146.242 }, { 51245.582,16488.814,43246.242 } } },
+ { .id = 24441, .name = "FORGOTTENARMORY_EP_04", .box = { { -15494.930,16379.199,43131.645 }, { -15394.930,16479.199,43231.645 } } },
+ { .id = 23111, .name = "FRONTLINE_EP_01", .box = { { 32402.020,9755.272,10406.177 }, { 32602.020,9855.272,10506.177 } } },
+ { .id = 23121, .name = "FRONTLINE_EP_02", .box = { { 32402.020,15875.220,10406.177 }, { 32602.020,16075.220,10506.177 } } },
+ { .id = 23131, .name = "FRONTLINE_EP_03", .box = { { 36353.066,26745.096,11463.154 }, { 36553.066,26945.096,11563.154 } } },
+ { .id = 23141, .name = "FRONTLINE_EP_04", .box = { { 32152.996,31490.635,10405.436 }, { 32352.996,31690.635,10505.436 } } },
+ { .id = 21121, .name = "GLUTONWORLD", .box = { { 7973.539,4432.462,511.082 }, { 8073.539,4532.462,611.082 } } },
+ { .id = 21051, .name = "GOLDENCITADEL_ATTRIBUTE", .box = { { 18630.664,17167.801,4917.395 }, { 19130.664,17667.801,5017.395 } } },
+ { .id = 10031, .name = "GRACECITY", .box = { { 42690.777,45873.383,4003.472 }, { 43390.777,46573.383,4103.472 } } },
+ { .id = 10051, .name = "GRASSCOVERCAMP", .box = { { 92328.406,36190.465,9170.499 }, { 92728.406,36590.465,9270.499 } } },
+ { .id = 26411, .name = "HOLYGROUND_EP_01", .box = { { 18899.211,74448.578,10505.964 }, { 18999.211,74548.578,10605.964 } } },
+ { .id = 26421, .name = "HOLYGROUND_EP_02", .box = { { 17072.000,65081.383,10397.242 }, { 17172.000,65181.383,10497.242 } } },
+ { .id = 26431, .name = "HOLYGROUND_EP_03", .box = { { 15204.037,67246.602,10408.680 }, { 15304.037,67346.602,10508.680 } } },
+ { .id = 26441, .name = "HOLYGROUND_EP_04", .box = { { 12404.172,68310.719,10406.670 }, { 12504.172,68410.719,10506.670 } } },
+ { .id = 21071, .name = "INNOCENTDAYDREAM", .box = { { 37215.270,19902.627,103.000 }, { 37415.270,20102.627,203.000 } } },
+ { .id = 21014, .name = "IRONCASTLE", .box = { { 9079.846,6939.715,5005.002 }, { 9179.846,7039.715,5105.002 } } },
+ { .id = 21014, .name = "IRONCASTLE_BLUE", .box = { { 9079.846,6939.715,5005.002 }, { 9179.846,7039.715,5105.002 } } },
+ { .id = 21014, .name = "IRONCASTLE_DARK", .box = { { 9079.846,6939.715,5005.002 }, { 9179.846,7039.715,5105.002 } } },
+ { .id = 21014, .name = "IRONCASTLE_GREEN", .box = { { 9079.846,6939.715,5005.002 }, { 9179.846,7039.715,5105.002 } } },
+ { .id = 21014, .name = "IRONCASTLE_LIGHT", .box = { { 9079.846,6939.715,5005.002 }, { 9179.846,7039.715,5105.002 } } },
+ { .id = 21014, .name = "IRONCASTLE_RED", .box = { { 9079.846,6939.715,5005.002 }, { 9179.846,7039.715,5105.002 } } },
+ { .id = 21014, .name = "IRONCASTLE_WHITE", .box = { { 9079.846,6939.715,5005.002 }, { 9179.846,7039.715,5105.002 } } },
+ { .id = 21012, .name = "JUNKHIVE", .box = { { 11757.481,19716.148,5547.168 }, { 11857.481,19816.148,5647.168 } } },
+ { .id = 21012, .name = "JUNKHIVE_BLUE", .box = { { 11757.481,19716.148,5547.168 }, { 11857.481,19816.148,5647.168 } } },
+ { .id = 21012, .name = "JUNKHIVE_DARK", .box = { { 11757.481,19716.148,5547.168 }, { 11857.481,19816.148,5647.168 } } },
+ { .id = 21012, .name = "JUNKHIVE_GREEN", .box = { { 11757.481,19716.148,5547.168 }, { 11857.481,19816.148,5647.168 } } },
+ { .id = 21012, .name = "JUNKHIVE_LIGHT", .box = { { 11757.481,19716.148,5547.168 }, { 11857.481,19816.148,5647.168 } } },
+ { .id = 21012, .name = "JUNKHIVE_RED", .box = { { 11757.481,19716.148,5547.168 }, { 11857.481,19816.148,5647.168 } } },
+ { .id = 21012, .name = "JUNKHIVE_WHITE", .box = { { 11757.481,19716.148,5547.168 }, { 11857.481,19816.148,5647.168 } } },
+ { .id = 22211, .name = "JUNKPOOL_EP_01", .box = { { 58542.820,91217.766,13228.068 }, { 58642.820,91317.766,13328.068 } } },
+ { .id = 22221, .name = "JUNKPOOL_EP_02", .box = { { 16546.227,20914.391,10627.595 }, { 16646.227,21014.391,10727.595 } } },
+ { .id = 22231, .name = "JUNKPOOL_EP_03", .box = { { 16902.471,21893.471,10252.203 }, { 17002.471,21993.471,10352.203 } } },
+ { .id = 22241, .name = "JUNKPOOL_EP_04", .box = { { 14310.878,34588.246,13161.544 }, { 14410.878,34688.246,13261.544 } } },
+ { .id = 21011, .name = "LASTCARNIVAL", .box = { { 88539.133,10735.693,1000.000 }, { 88639.133,10835.693,1100.000 } } },
+ { .id = 24011, .name = "LASTCARNIVAL_BLUE", .box = { { 88539.133,10735.693,1000.000 }, { 88639.133,10835.693,1100.000 } } },
+ { .id = 23011, .name = "LASTCARNIVAL_DARK", .box = { { 88539.133,10735.693,1000.000 }, { 88639.133,10835.693,1100.000 } } },
+ { .id = 27011, .name = "LASTCARNIVAL_GREEN", .box = { { 88539.133,10735.693,1000.000 }, { 88639.133,10835.693,1100.000 } } },
+ { .id = 22011, .name = "LASTCARNIVAL_LIGHT", .box = { { 88539.133,10735.693,1000.000 }, { 88639.133,10835.693,1100.000 } } },
+ { .id = 25011, .name = "LASTCARNIVAL_RED", .box = { { 88539.133,10735.693,1000.000 }, { 88639.133,10835.693,1100.000 } } },
+ { .id = 25011, .name = "LASTCARNIVAL_WHITE", .box = { { 88539.133,10735.693,1000.000 }, { 88639.133,10835.693,1100.000 } } },
+ { .id = 21131, .name = "LONELYCHRISTMAS", .box = { { 39545.043,34364.980,10025.864 }, { 39645.043,34464.980,10125.864 } } },
+ { .id = 25211, .name = "MANEATERGARDEN_EP_01", .box = { { 10116.053,27610.877,7528.966 }, { 10216.053,27710.877,7628.966 } } },
+ { .id = 25321, .name = "MANEATERGARDEN_EP_02", .box = { { 9460.236,29377.686,8458.003 }, { 9560.236,29477.686,8558.003 } } },
+ { .id = 25331, .name = "MANEATERGARDEN_EP_03", .box = { { 7182.635,27487.670,7528.966 }, { 7282.635,27587.670,7628.966 } } },
+ { .id = 25341, .name = "MANEATERGARDEN_EP_04", .box = { { 3921.480,29388.898,8458.003 }, { 4021.480,29488.898,8558.003 } } },
+ { .id = 21151, .name = "MEMORIALHALL", .box = { { 40449.270,25302.699,3998.000 }, { 40549.270,25402.699,4098.000 } } },
+ { .id = 21161, .name = "MEMORIALHALL_NEW", .box = { { 40449.270,25302.699,3998.000 }, { 40549.270,25402.699,4098.000 } } },
+ { .id = 20011, .name = "MYROOM", .box = { { 685.412,-627.910,3717.242 }, { 785.412,-527.910,3817.242 } } },
+ { .id = 22411, .name = "N102SHELTER_EP_01", .box = { { 4353.347,13664.854,1001.668 }, { 4453.347,13764.854,1101.668 } } },
+ { .id = 22421, .name = "N102SHELTER_EP_02", .box = { { 4353.347,13664.854,1001.668 }, { 4453.347,13764.854,1101.668 } } },
+ { .id = 22431, .name = "N102SHELTER_EP_03", .box = { { 9949.543,19557.410,1001.668 }, { 10049.543,19657.410,1101.668 } } },
+ { .id = 22441, .name = "N102SHELTER_EP_04", .box = { { 4374.573,25620.955,1001.668 }, { 4474.573,25720.955,1101.668 } } },
+ { .id = 23211, .name = "NEDCOMPANY_EP_01", .box = { { 21500.287,2064.431,46.542 }, { 21600.287,2164.431,146.542 } } },
+ { .id = 23221, .name = "NEDCOMPANY_EP_02", .box = { { 130.467,18080.516,46.541 }, { 230.467,18180.516,146.541 } } },
+ { .id = 23231, .name = "NEDCOMPANY_EP_03", .box = { { 2503.138,11995.378,5175.642 }, { 2603.138,12095.378,5275.642 } } },
+ { .id = 23241, .name = "NEDCOMPANY_EP_04", .box = { { 2625.382,18907.770,46.541 }, { 2725.382,19007.770,146.541 } } },
+ { .id = 25211, .name = "PERFORATEDSTREET_EP_01", .box = { { 17903.576,29655.496,4577.099 }, { 18003.576,29755.496,4677.099 } } },
+ { .id = 25221, .name = "PERFORATEDSTREET_EP_02", .box = { { 17179.973,30236.596,4576.098 }, { 17279.973,30336.596,4676.098 } } },
+ { .id = 25231, .name = "PERFORATEDSTREET_EP_03", .box = { { 18533.256,32822.043,6479.744 }, { 18633.256,32922.043,6579.744 } } },
+ { .id = 25241, .name = "PERFORATEDSTREET_EP_04", .box = { { 14017.632,33327.297,6695.974 }, { 14117.632,33427.297,6795.974 } } },
+ { .id = 10003, .name = "ROCCOTOWN", .box = { { 10230.798,10058.951,90.452 }, { 10630.798,10458.951,190.452 } } },
+ { .id = 21311, .name = "RSQUARE_EP_01", .box = { { 29821.670,19728.236,103.000 }, { 30021.670,19928.236,203.000 } } },
+ { .id = 21321, .name = "RSQUARE_EP_02", .box = { { 23113.805,28637.383,103.740 }, { 23313.805,28837.383,203.740 } } },
+ { .id = 21331, .name = "RSQUARE_EP_03", .box = { { 17325.986,31142.398,100.000 }, { 17525.986,31342.398,200.000 } } },
+ { .id = 21341, .name = "RSQUARE_EP_04", .box = { { 20078.764,30885.338,101.646 }, { 20178.764,30985.338,201.646 } } },
+ { .id = 10041, .name = "RUINFORTRESS", .box = { { 23580.543,21138.908,3665.746 }, { 24380.543,21938.908,3865.746 } } },
+ { .id = 21171, .name = "RUMBLEVACATION", .box = { { 44935.207,48635.254,8572.622 }, { 44035.207,48735.254,8672.622 } } },
+ { .id = 20101, .name = "S01_STEELGRAVE", .box = { { 18188.992,6668.367,4040.148 }, { 18288.992,6768.367,4140.147 } } },
+ { .id = 20201, .name = "S02_STEELGRAVE", .box = { { 18188.992,6652.533,4040.148 }, { 18288.992,6752.533,4140.147 } } },
+ { .id = 20201, .name = "S03_STEELGRAVE", .box = { { 18188.992,6652.533,4040.148 }, { 18288.992,6752.533,4140.147 } } },
+ { .id = 21141, .name = "SKYCLOCKPALACE", .box = { { 7960.293,5898.859,500.000 }, { 8060.293,5998.859,600.000 } } },
+ { .id = 26111, .name = "SKYWALKER_EP_01", .box = { { 24683.631,44180.051,75493.656 }, { 24783.631,44280.051,75593.656 } } },
+ { .id = 26121, .name = "SKYWALKER_EP_02", .box = { { 15332.727,48277.594,74982.211 }, { 15432.727,48377.594,75082.211 } } },
+ { .id = 26131, .name = "SKYWALKER_EP_03", .box = { { 23075.510,49482.254,74981.094 }, { 23175.510,49582.254,75081.094 } } },
+ { .id = 26141, .name = "SKYWALKER_EP_04", .box = { { 21541.547,47776.758,74981.680 }, { 21641.547,47876.758,75081.680 } } },
+ { .id = 21111, .name = "T01_TUTORIAL", .box = { { 90376.422,83963.500,26236.639 }, { 90476.422,84063.500,26336.639 } } },
+ { .id = 21112, .name = "T02_TUTORIAL", .box = { { 2970.878,2450.023,3751.836 }, { 3070.878,2550.023,3851.836 } } },
+ { .id = 21113, .name = "T03_TUTORIAL", .box = { { 19050.854,32111.730,9052.000 }, { 19150.854,32211.730,9152.000 } } },
+ { .id = 10001, .name = "TESTFIELD", .box = { { 17023.758,12062.019,3849.282 }, { 19023.758,14062.019,4149.282 } } },
+ { .id = 22431, .name = "THEBIGMOUTH_EP_01", .box = { { 3477.031,12583.577,4655.882 }, { 3577.031,12683.577,4755.882 } } },
+ { .id = 22321, .name = "THEBIGMOUTH_EP_02", .box = { { 7704.808,14387.982,4655.882 }, { 7804.808,14487.982,4755.882 } } },
+ { .id = 22331, .name = "THEBIGMOUTH_EP_03", .box = { { 16668.258,9277.498,5387.734 }, { 16768.258,9377.498,5487.734 } } },
+ { .id = 22341, .name = "THEBIGMOUTH_EP_04", .box = { { 25708.998,19023.846,6417.370 }, { 25808.998,19123.846,6517.370 } } },
+ { .id = 21061, .name = "THEPRIMAL", .box = { { 19970.262,14934.548,10689.821 }, { 20070.262,15034.548,10789.821 } } },
+ { .id = 21062, .name = "THEPRIMAL_EV", .box = { { 19970.262,14934.548,10689.821 }, { 20070.262,15034.548,10789.821 } } },
+ { .id = 21511, .name = "TOWEROFGREED_EP_01", .box = { { 2895.304,7615.922,1020.000 }, { 2995.304,7715.922,1120.000 } } },
+ { .id = 21521, .name = "TOWEROFGREED_EP_02", .box = { { 2900.648,2807.235,1158.859 }, { 3000.648,2907.235,1258.859 } } },
+ { .id = 21531, .name = "TOWEROFGREED_EP_03", .box = { { -578.867,2820.871,1000.000 }, { -478.867,2920.871,1100.000 } } },
+ { .id = 21541, .name = "TOWEROFGREED_EP_04", .box = { { 2911.050,7461.119,1030.000 }, { 3011.050,7561.119,1130.000 } } },
+ { .id = 26211, .name = "TRANSPORTFLEET_EP_01", .box = { { 21268.977,87809.406,5999.974 }, { 21368.977,87909.406,6099.974 } } },
+ { .id = 26221, .name = "TRANSPORTFLEET_EP_02", .box = { { 37043.418,25033.854,5159.079 }, { 37143.418,25133.854,5259.079 } } },
+ { .id = 26231, .name = "TRANSPORTFLEET_EP_03", .box = { { 26224.074,74082.203,4734.167 }, { 26324.074,74182.203,4834.167 } } },
+ { .id = 26241, .name = "TRANSPORTFLEET_EP_04", .box = { { 43514.754,72758.508,6418.685 }, { 43614.754,72858.508,6518.685 } } },
+ { .id = 23311, .name = "WOLFCRY_EP_01", .box = { { 16683.711,70794.195,1999.813 }, { 16783.711,70894.195,2099.813 } } },
+ { .id = 23321, .name = "WOLFCRY_EP_02", .box = { { 8451.205,64828.133,1999.813 }, { 8551.205,64928.133,2099.813 } } },
+ { .id = 23331, .name = "WOLFCRY_EP_03", .box = { { 22846.598,33700.957,3776.000 }, { 22946.598,33800.957,3876.000 } } },
+ { .id = 23341, .name = "WOLFCRY_EP_04", .box = { { 35017.801,43299.293,3775.757 }, { 35117.801,43399.293,3875.757 } } },
+};
+
diff --git a/src/login.c b/src/login.c
new file mode 100644
index 0000000..a804d33
--- /dev/null
+++ b/src/login.c
@@ -0,0 +1,362 @@
+#include "db.h"
+#include "packet.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <err.h>
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
+
+static struct {
+ struct { struct db login; } db;
+ uint32_t session;
+} server;
+
+static void
+flush_packet(union packet *packet)
+{
+ packet_crypt(packet);
+ fwrite(packet->buf, 1, packet->hdr.size, stdout);
+}
+
+static void
+handle_select_character_req(union packet *packet, bool prologue)
+{
+ uint32_t char_id;
+ {
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_read(&pbuf, &char_id, sizeof(char_id));
+ }
+
+ // TODO: check that account actually has the character
+
+ db_query_single(&server.db.login, "update sessions set char_id = ? where id = ?",
+ (struct db_query_arg[]){
+ { .type = DB_ARG_I32, .u.i32 = char_id },
+ { .type = DB_ARG_I32, .u.i32 = server.session }
+ }, 2, NULL, 0);
+
+ struct db_query_arg args[] = {
+ { .type = DB_ARG_I32, .u.i32 = server.session }
+ }, cols[2];
+
+ const int results = db_query_single(&server.db.login,
+ "select host, port from servers where name = (select server from sessions where id = ?) limit 1",
+ args, ARRAY_SIZE(args), cols, ARRAY_SIZE(cols));
+
+ assert(results == ARRAY_SIZE(cols));
+ assert(cols[0].type == DB_ARG_UTF8 || cols[0].type == DB_ARG_NULL);
+ assert(cols[1].type == DB_ARG_I32);
+
+ const char *host = getenv("TCPREMOTEIP");
+ if (!host || strcmp(host, "127.0.0.1")) {
+ const char *host = getenv("GAME_SERVER");
+ host = (host ? host : cols[0].u.blob.data);
+ }
+
+ warnx("connecting to game server: %s:%u", (host ? host : (char*)cols[0].u.blob.data), cols[1].u.i32);
+
+ packet->hdr.type = (prologue ? PACKET_CHARACTER_ENTER_PROLOGUE : PACKET_SELECT_CHARACTER_RES);
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, &char_id, sizeof(char_id));
+ for (size_t i = 0; i < 32; ++i) pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_write_str(&pbuf, (host ? host : cols[0].u.blob.data)); // server ip
+ pbuf_write(&pbuf, (uint16_t[]){cols[1].u.i32}, sizeof(uint16_t)); // port
+ for (size_t i = 0; i < 38; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(packet);
+}
+
+static void
+handle_create_character_req(union packet *packet)
+{
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf.cursor += 4; // unknown;
+
+ struct pbuf_string name;
+ uint8_t model;
+ pbuf_read_str_utf16(&pbuf, &name);
+ pbuf_read(&pbuf, &model, 1);
+
+ db_query_single(&server.db.login,
+ "insert into characters (account_id, model, name) values ((select account_id from sessions where id = ? limit 1), ?, ?)",
+ (struct db_query_arg[]){
+ { .type = DB_ARG_I32, .u.i32 = server.session },
+ { .type = DB_ARG_I32, .u.i32 = model },
+ { .type = DB_ARG_UTF8, .u.blob = { .data = name.data, .sz = name.len } }
+ }, 3, NULL, 0);
+}
+
+static void
+handle_delete_character_req(union packet *packet)
+{
+ packet->hdr.type = PACKET_DELETE_CHARACTER_RES;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_flush(&pbuf);
+ flush_packet(packet);
+}
+
+static void
+handle_charlist(union packet *packet)
+{
+ packet->hdr.type = PACKET_CHARACTER_LIST_RES;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+
+ struct db_query_arg args[] = {{ .type = DB_ARG_I32, .u.i32 = server.session }}, cols[5];
+ struct db_query query = db_query_begin(&server.db.login,
+ "select id, model, name, evolution, lvl from characters where account_id = (select account_id from sessions where id = ? limit 1) order by id",
+ args, ARRAY_SIZE(args));
+
+ pbuf_write(&pbuf, (uint8_t[]){0}, 1); // num characters (placeholder)
+ for (uint8_t i = 0; db_query_fetch(&query, cols, ARRAY_SIZE(cols)) == ARRAY_SIZE(cols); ++i) {
+ assert(cols[0].type == DB_ARG_I32 && cols[1].type == DB_ARG_I32 && cols[3].type == DB_ARG_I32 && cols[4].type == DB_ARG_I32);
+ assert(cols[2].type == DB_ARG_UTF8);
+ pbuf_write(&pbuf, (uint32_t[]){cols[0].u.i32}, sizeof(uint32_t)); // charid
+ pbuf_write_str_len_utf16(&pbuf, cols[2].u.blob.data, cols[2].u.blob.sz); // char name UTF16
+ pbuf_write(&pbuf, (uint8_t[]){cols[1].u.i32}, 1); // char model (lily, haru, ...), 0 for no char
+ pbuf_write(&pbuf, (uint8_t[]){cols[3].u.i32}, 1); // evolution lvl
+ for (size_t i = 0; i < 20; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // skin/eye/body/etc
+ pbuf_write(&pbuf, (uint8_t[]){cols[4].u.i32}, 1); // char lvl
+ for (size_t i = 0; i < 526; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // fashion?
+ pbuf_write(&pbuf, (uint8_t[]){i + 1}, 1); // char slot
+ packet->buf[sizeof(packet->hdr)] = i + 1;
+ }
+ pbuf_flush(&pbuf);
+ flush_packet(packet);
+}
+
+static void
+handle_enter_server_req(union packet *packet)
+{
+ {
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_read(&pbuf, &server.session, sizeof(server.session));
+ }
+
+ packet->hdr.type = PACKET_ENTER_SERVER_RES;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // error code maybe?
+ pbuf_write(&pbuf, &server.session, sizeof(server.session));
+ pbuf_write(&pbuf, (uint8_t[]){0xff, 0xff}, 2); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(packet);
+
+ packet->hdr.type = PACKET_SYSTEM_SERVER_OPTION_UPDATE;
+ pbuf = (struct pbuf){ .packet = packet, .cursor = sizeof(packet->hdr) };
+ // TODO: figure out what these options are, some of them disable second password
+ pbuf_write(&pbuf, (uint8_t[]){0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01}, 14);
+ pbuf_flush(&pbuf);
+ flush_packet(packet);
+}
+
+static void
+handle_server_connect_req(union packet *packet)
+{
+ {
+ uint8_t selected;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_read(&pbuf, &selected, sizeof(selected));
+ db_query_single(&server.db.login, "update sessions set server = (select name from servers order by id limit 1 offset ?) where id = ?",
+ (struct db_query_arg[]){
+ { .type = DB_ARG_I32, .u.i32 = (int)selected + 1 },
+ { .type = DB_ARG_I32, .u.i32 = server.session }
+ }, 2, NULL, 0);
+
+ warnx("moving to the authentication part: %u", selected);
+ }
+
+ packet->hdr.type = PACKET_ENTER_SERVER;
+ packet->hdr.size = sizeof(packet->hdr);
+ flush_packet(packet);
+}
+
+static void
+handle_server_list_req(union packet *packet)
+{
+ uint8_t num_chars;
+ {
+ struct db_query_arg col = {0};
+ db_query_single(&server.db.login,
+ "select count(id) from characters where account_id = (select account_id from sessions where id = ? limit 1)",
+ (struct db_query_arg[]){{ .type = DB_ARG_I32, .u.i32 = server.session }}, 1, &col, 1);
+ num_chars = col.u.i32;
+ }
+
+ uint16_t port;
+ const char *host = getenv("TCPREMOTEIP");
+ {
+ struct db_query_arg cols[2];
+ const int results = db_query_single(&server.db.login,
+ "select host, port from servers limit 1",
+ NULL, 0, cols, ARRAY_SIZE(cols));
+
+ assert(results == ARRAY_SIZE(cols));
+ assert(cols[0].type == DB_ARG_UTF8 || cols[0].type == DB_ARG_NULL);
+ assert(cols[1].type == DB_ARG_I32);
+ port = cols[1].u.i32;
+
+ if (!host || strcmp(host, "127.0.0.1"))
+ host = (cols[0].u.blob.data ? cols[0].u.blob.data : "127.0.0.1");
+ }
+
+ packet->hdr.type = PACKET_SERVER_LIST;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // num servers (placeholder)
+
+ struct db_query_arg cols[2];
+ struct db_query query = db_query_begin(&server.db.login, "select name, status from servers order by id limit 255 offset 1", NULL, 0);
+ for (uint8_t i = 0; db_query_fetch(&query, cols, ARRAY_SIZE(cols)) == ARRAY_SIZE(cols); ++i) {
+ assert(cols[0].type == DB_ARG_UTF8);
+ assert(cols[1].type == DB_ARG_I32);
+ warnx("%s: %u", (char*)cols[0].u.blob.data, cols[1].u.i32);
+ pbuf_write(&pbuf, (uint8_t[]){i}, 1); // server index
+ pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_write(&pbuf, &port, sizeof(port)); // port
+ pbuf_write_str_len(&pbuf, cols[0].u.blob.data, cols[0].u.blob.sz); // server name
+ pbuf_write_str(&pbuf, host); // server ip
+ pbuf_write(&pbuf, (uint32_t[]){cols[1].u.i32}, sizeof(uint32_t)); // server status
+ for (size_t i = 0; i < 4; ++i) pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_write(&pbuf, &num_chars, 1); // num characters
+ packet->buf[sizeof(packet->hdr) + 1] = i + 1;
+ }
+ db_query_end(&query);
+ pbuf_flush(&pbuf);
+ flush_packet(packet);
+}
+
+static void
+handle_login_req(union packet *packet)
+{
+ struct pbuf_string username, password, mac = {0};
+ {
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_read_str_utf16(&pbuf, &username);
+ pbuf_read_str_utf16(&pbuf, &password);
+ pbuf_read_str_utf16(&pbuf, &mac);
+ }
+
+ struct db_query_arg args[] = {
+ { .type = DB_ARG_UTF8, .u.blob = { .data = username.data, .sz = username.len } },
+ { .type = DB_ARG_UTF8, .u.blob = { .data = password.data, .sz = password.len } },
+ }, cols[1];
+
+ const int results = db_query_single(&server.db.login,
+ "select id from accounts where username = ? and password = ? limit 1",
+ args, ARRAY_SIZE(args), cols, ARRAY_SIZE(cols));
+
+ warnx("%s login: %d", username.data, results);
+
+ enum {
+ OK = 0x00,
+ INVALID_CREDENTIALS = 0x01,
+ ALREADY_CONNECTED = 0x02,
+ ACCOUNT_BLOCKED = 0x03,
+ IP_BLOCKED = 0x04,
+ MAC_BLOCKED = 0x05,
+ MAC_INVALID = 0x06,
+ } status = OK;
+
+ if (mac.len != 17) {
+ status = MAC_INVALID;
+ } else if (!results) {
+ status = INVALID_CREDENTIALS;
+ } else {
+ const uint32_t account_id = cols[0].u.i32;
+
+ db_query_single(&server.db.login,
+ "update accounts set last_login = current_timestamp where username = ? and password = ?",
+ args, ARRAY_SIZE(args), NULL, 0);
+
+ do {
+ server.session = rand();
+ } while (db_query_single(&server.db.login, "select id from sessions where id = ?",
+ (struct db_query_arg[]){{ .type = DB_ARG_I32, .u.i32 = server.session }}, 1, NULL, 0));
+
+ db_query_single(&server.db.login,
+ "insert into sessions (id, account_id, server) values (?, ?, (select name from servers order by id limit 1))",
+ (struct db_query_arg[]){
+ { .type = DB_ARG_I32, .u.i32 = server.session },
+ { .type = DB_ARG_I32, .u.i32 = account_id }
+ }, 2, NULL, 0);
+ }
+
+ packet->hdr.type = PACKET_LOGIN_RESULT;
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, &server.session, sizeof(uint32_t)); // session token
+ pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_write(&pbuf, mac.data, 17); // mac
+ for (size_t i = 0; i < 3; ++i) pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_write(&pbuf, (uint8_t[]){status}, 1); // login status
+ for (size_t i = 0; i < 4; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown (but needs to be 0)
+ pbuf_write_str_len_utf16(&pbuf, username.data, username.len); // username
+ for (size_t i = 0; i < 13; ++i) pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ pbuf_flush(&pbuf);
+ flush_packet(packet);
+}
+
+int
+main(void)
+{
+ setvbuf(stdout, NULL, _IONBF, 0);
+ db_init(&server.db.login, "login.db");
+ server.session = ~(uint32_t)0;
+
+ union packet packet;
+ for (size_t psz; (psz = fread(packet.buf, 1, sizeof(packet.hdr), stdin)) > 0; ) {
+ if (!packet_verify(&packet))
+ errx(EXIT_FAILURE, "invalid packet");
+
+ psz += fread(packet.buf + sizeof(packet.hdr), 1, packet.hdr.size - sizeof(packet.hdr), stdin);
+ if (psz != packet.hdr.size)
+ errx(EXIT_FAILURE, "packet size doesn't match, got %zu, expected %u", psz, packet.hdr.size);
+
+ packet_crypt(&packet);
+ warnx("got packet of type 0x%.4x", packet.hdr.type);
+
+ switch (packet.hdr.type) {
+ /** LOGIN */
+ case PACKET_LOGIN_REQ:
+ handle_login_req(&packet);
+ break;
+ case PACKET_SERVER_LIST_REQ:
+ handle_server_list_req(&packet);
+ break;
+ case PACKET_SERVER_CONNECT_REQ:
+ handle_server_connect_req(&packet);
+ break;
+ /** AUTH */
+ case PACKET_ENTER_SERVER_REQ:
+ handle_enter_server_req(&packet);
+ break;
+ case PACKET_CHARACTER_LIST_REQ:
+ handle_charlist(&packet);
+ break;
+ case PACKET_CHARACTER_UPDATE_SPECIAL_OPTION_LIST: // ???
+ break;
+ case PACKET_SECOND_PASSWORD:
+ warnx("we shouldn't get this packet as we told client to not send it");
+ break;
+ case PACKET_CREATE_CHARACTER_REQ:
+ handle_create_character_req(&packet);
+ handle_charlist(&packet);
+ break;
+ case PACKET_DELETE_CHARACTER_REQ:
+ handle_delete_character_req(&packet);
+ handle_charlist(&packet);
+ break;
+ case PACKET_SELECT_CHARACTER_REQ:
+ handle_select_character_req(&packet, false);
+ break;
+ default:
+ warnx("unknown packet");
+ break;
+ }
+
+ db_gc(&server.db.login);
+ }
+
+ db_release(&server.db.login);
+ return EXIT_SUCCESS;
+}
diff --git a/src/mitm.c b/src/mitm.c
new file mode 100644
index 0000000..5966de4
--- /dev/null
+++ b/src/mitm.c
@@ -0,0 +1,243 @@
+#include "packet.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <err.h>
+
+#include <poll.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <stdbool.h>
+
+struct proc {
+ pid_t pid;
+ int fds[2];
+};
+
+static void
+close_fd(int *fd)
+{
+ if (*fd >= 0)
+ close(*fd);
+}
+
+static void
+proc_close(struct proc *proc)
+{
+ if (proc->pid)
+ waitpid(proc->pid, NULL, 0);
+
+ close_fd(&proc->fds[0]);
+ close_fd(&proc->fds[1]);
+ *proc = (struct proc){0};
+}
+
+static bool
+proc_open(const char *file, char *const argv[], struct proc *out_proc)
+{
+ *out_proc = (struct proc){0};
+
+ int pipes[4];
+ if (pipe(&pipes[0]) != 0 /* parent */ || pipe(&pipes[2]) != 0 /* child */) {
+ proc_close(out_proc);
+ return false;
+ }
+
+ if ((out_proc->pid = fork()) > 0) {
+ out_proc->fds[0] = pipes[3];
+ out_proc->fds[1] = pipes[0];
+ close(pipes[1]);
+ close(pipes[2]);
+ return true;
+ } else {
+ close(pipes[0]);
+ close(pipes[3]);
+ dup2(pipes[2], 0);
+ dup2(pipes[1], 1);
+ close(pipes[2]);
+ close(pipes[1]);
+ execvp(file, argv);
+ _exit(0);
+ }
+
+ out_proc->fds[0] = pipes[3];
+ out_proc->fds[1] = pipes[0];
+ close(pipes[1]);
+ close(pipes[2]);
+ return true;
+}
+
+static size_t
+safe_read(int fd, void *dst, const size_t dst_size)
+{
+ size_t off = 0;
+ ssize_t ret = 0;
+ for (; off < dst_size && (ret = read(fd, (char*)dst + off, dst_size - off)) > 0; off += ret);
+ return (ret < 0 ? 0 : off);
+}
+
+static void
+dump_packet(const uint8_t *bytes, const size_t sz, const char *prefix, const uint16_t type)
+{
+ const char *local = getenv("TCPLOCALPORT");
+
+ char path[4096];
+ snprintf(path, sizeof(path), "packets/%s-%s-0x%.4x.raw", local, prefix, type);
+
+ FILE *f;
+ if (!(f = fopen(path, "wb")))
+ err(EXIT_FAILURE, "fopen");
+
+ if (fwrite(bytes, 1, sz, f) != sz)
+ err(EXIT_FAILURE, "fwrite");
+
+ fclose(f);
+}
+
+static void
+handle_select_character_res(union packet *packet, bool prologue)
+{
+ packet->hdr.type = (prologue ? PACKET_CHARACTER_ENTER_PROLOGUE : PACKET_SELECT_CHARACTER_RES);
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf.cursor += sizeof(uint32_t); // char id
+ for (size_t i = 0; i < 32; ++i) pbuf_write(&pbuf, (uint8_t[]){0xff}, 1); // unknown
+ const char *server_ip = getenv("SERVER_IP");
+ server_ip = (server_ip ? server_ip : "127.0.0.1");
+ pbuf_write_str(&pbuf, server_ip); // server ip
+ pbuf_write(&pbuf, (uint16_t[]){10100}, sizeof(uint16_t)); // port
+ for (size_t i = 0; i < 38; ++i) pbuf_write(&pbuf, (uint8_t[]){0x00}, 1); // unknown
+ pbuf_flush(&pbuf);
+}
+
+static void
+handle_character_enter_prologue(union packet *packet)
+{
+ handle_select_character_res(packet, true);
+}
+
+static void
+handle_server_list(union packet *packet)
+{
+ struct pbuf pbuf = { .packet = packet, .cursor = sizeof(packet->hdr) };
+ pbuf_write(&pbuf, (uint8_t[]){0x00, 0x01, 0x00, 0x00}, 4); // unknown
+ pbuf_write(&pbuf, (uint16_t[]){10000}, sizeof(uint16_t)); // port
+ pbuf_write_str(&pbuf, "benis"); // server name
+ const char *server_ip = getenv("SERVER_IP");
+ server_ip = (server_ip ? server_ip : "127.0.0.1");
+ pbuf_write_str(&pbuf, server_ip); // server ip
+ pbuf_write(&pbuf, (uint8_t[]){0x01, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x00}, 9);
+ pbuf_flush(&pbuf);
+}
+
+static bool
+intercept(union packet *packet)
+{
+ packet_crypt(packet);
+
+ bool ret = true;
+ switch (packet->hdr.type) {
+ case PACKET_SERVER_LIST:
+ handle_server_list(packet);
+ break;
+ case PACKET_SELECT_CHARACTER_RES:
+ handle_select_character_res(packet, false);
+ break;
+ case PACKET_CHARACTER_ENTER_PROLOGUE:
+ handle_character_enter_prologue(packet);
+ break;
+ default:
+ ret = false;
+ break;
+ }
+
+ packet_crypt(packet);
+ return ret;
+}
+
+static uint16_t
+decrypted_packet_type(union packet *packet)
+{
+ const uint16_t size = packet->hdr.size;
+ packet->hdr.size = sizeof(packet->hdr);
+ packet_crypt(packet);
+ const uint16_t type = packet->hdr.type;
+ packet_crypt(packet);
+ packet->hdr.size = size;
+ return type;
+}
+
+int
+main(int argc, char *const argv[])
+{
+ for (int i = 1; i < argc; ++i) {
+ if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h"))
+ errx(EXIT_SUCCESS, "usage: %s cmd [args]", argv[0]);
+ }
+
+ struct proc server = {0};
+ if (argc >= 2 && !proc_open(argv[1], argv + 1, &server))
+ errx(EXIT_FAILURE, "failed to execute: %s", argv[1]);
+
+ setvbuf(stdout, NULL, _IONBF, 0);
+
+ struct pollfd pfd[] = {
+ { .fd = STDIN_FILENO, .events = POLLIN },
+ { .fd = server.fds[1], .events = POLLIN },
+ };
+
+ union packet packet;
+ while (poll(pfd, 1 + !!server.pid, -1) > 0) {
+ if (pfd[0].revents & POLLIN) {
+ if (fread(packet.buf, 1, sizeof(packet.hdr), stdin) < sizeof(packet.hdr))
+ errx(EXIT_SUCCESS, "client close");
+
+ if (!packet_verify(&packet)) {
+ for (size_t i = 0; i < 7; ++i) fprintf(stderr, "%.2x ", packet.buf[i]);
+ errx(EXIT_FAILURE, "invalid packet");
+ }
+
+ if (fread(packet.buf + sizeof(packet.hdr), 1, packet.hdr.size - sizeof(packet.hdr), stdin) != packet.hdr.size - sizeof(packet.hdr))
+ errx(EXIT_SUCCESS, "failed to read packet from client");
+
+ const uint16_t type = decrypted_packet_type(&packet);
+ dump_packet(packet.buf, packet.hdr.size, "client", type);
+
+ if (intercept(&packet)) {
+ dump_packet(packet.buf, packet.hdr.size, "client-intercepted", type);
+ warnx("intercepted client packet 0x%.4x", type);
+ } else {
+ warnx("client packet 0x%.4x (%u)",type, packet.hdr.size);
+ }
+
+ if (server.pid && write(server.fds[0], packet.buf, packet.hdr.size) != packet.hdr.size)
+ err(EXIT_FAILURE, "failed to write to server");
+ } else if (pfd[1].revents & POLLIN) {
+ if (read(server.fds[1], packet.buf, sizeof(packet.hdr)) < (ssize_t)sizeof(packet.hdr))
+ errx(EXIT_SUCCESS, "server close");
+
+ if (!packet_verify(&packet)) {
+ for (size_t i = 0; i < 7; ++i) fprintf(stderr, "%.2x ", packet.buf[i]);
+ errx(EXIT_FAILURE, "invalid packet");
+ }
+
+ if (safe_read(server.fds[1], packet.buf + sizeof(packet.hdr), packet.hdr.size - sizeof(packet.hdr)) != (packet.hdr.size - sizeof(packet.hdr)))
+ errx(EXIT_SUCCESS, "failed to read packet from server");
+
+ const uint16_t type = decrypted_packet_type(&packet);
+ dump_packet(packet.buf, packet.hdr.size, "server", type);
+
+ if (intercept(&packet)) {
+ dump_packet(packet.buf, packet.hdr.size, "server-intercepted", type);
+ warnx("intercepted server packet 0x%.4x",type);
+ } else {
+ warnx("server packet 0x%.4x (%u)", type, packet.hdr.size);
+ }
+
+ if (fwrite(packet.buf, 1, packet.hdr.size, stdout) != packet.hdr.size)
+ err(EXIT_FAILURE, "failed to write to client");
+ }
+ }
+
+ proc_close(&server);
+ return EXIT_SUCCESS;
+}
diff --git a/src/packet.c b/src/packet.c
new file mode 100644
index 0000000..8ab6ae6
--- /dev/null
+++ b/src/packet.c
@@ -0,0 +1,167 @@
+#include "packet.h"
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <err.h>
+
+#include <iconv.h>
+
+_Static_assert(sizeof(((union packet*)0)->buf) >= sizeof(((union packet*)0)->hdr), "packet.hdr must fit packet");
+_Static_assert(sizeof(((union packet*)0)->buf) == sizeof(*((union packet*)0)), "packet.buf must be sizeof(packet)");
+
+bool
+packet_verify(const union packet *packet)
+{
+ assert(packet);
+
+ if (packet->hdr.size < sizeof(packet->hdr) || packet->hdr.size > sizeof(packet->buf))
+ return false;
+
+ static const uint8_t hdr[] = { 0x02, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff }; // 0xff are wildcards
+ _Static_assert(sizeof(packet->hdr) >= sizeof(hdr), "sizeof(packet->hdr) != sizeof(hdr)");
+
+ for (const uint8_t *b = packet->buf, *h = hdr; (size_t)(h - hdr) < sizeof(hdr) ; ++b, ++h) {
+ if (*h != 0xff && *b != *h)
+ return false;
+ }
+
+ return true;
+}
+
+void
+packet_crypt(union packet *packet)
+{
+ assert(packet);
+ static const uint8_t key[] = { 0x57, 0x19, 0xc6, 0x2d, 0x56, 0x68, 0x3a, 0xcc, 0x60, 0x3b, 0x0b, 0xb1, 0x90, 0x5c, 0x4a, 0xf8, 0x80, 0x28, 0xb1, 0x45, 0xb6, 0x85, 0xe7, 0x4c, 0x06, 0x2d, 0x55, 0x83, 0xaf, 0x44, 0x99 };
+ const int16_t salt = packet->hdr.salt + packet->hdr.salt + packet->hdr.salt * 2;
+ for (size_t i = 5 /* skip salt && size && unknown[0] */, c = 0; i < packet->hdr.size; ++i, ++c) {
+ const uint32_t hi = (uint64_t)(0x55555556 * c) >> 32;
+ int32_t magic = (hi >> sizeof(key)) + hi;
+ magic = salt - (magic + magic * 2);
+ assert(magic + c < sizeof(key));
+ packet->buf[i] ^= key[magic + c];
+ }
+}
+
+static size_t
+utf16_to_utf8(const uint8_t *utf16, const size_t utf16_sz, uint8_t *utf8, const size_t utf8_sz)
+{
+ assert(utf16 && utf8 && utf8_sz);
+
+ if (!utf16_sz) {
+ utf8[0] = 0;
+ return 0;
+ }
+
+ static iconv_t cd;
+ if (!cd && !(cd = iconv_open("utf8", "utf16le")))
+ err(EXIT_FAILURE, "iconv_open");
+
+ size_t isz = utf16_sz, osz = utf8_sz - 1;
+ iconv(cd, (char*[]){(uint8_t*)utf16}, &isz, (char*[]){utf8}, &osz);
+ const size_t len = (utf8_sz - 1) - osz;
+ utf8[len] = 0;
+ return len;
+}
+
+static size_t
+utf8_to_utf16(const uint8_t *utf8, const size_t utf8_sz, uint8_t *utf16, const size_t utf16_sz)
+{
+ assert(utf8 && utf16 && utf16_sz);
+
+ if (!utf8_sz)
+ return 0;
+
+ static iconv_t cd;
+ if (!cd && !(cd = iconv_open("utf16le", "utf8")))
+ err(EXIT_FAILURE, "iconv_open");
+
+ size_t isz = utf8_sz, osz = utf16_sz;
+ iconv(cd, (char*[]){(uint8_t*)utf8}, &isz, (char*[]){utf16}, &osz);
+ const size_t len = utf16_sz - osz;
+ return len;
+}
+
+void
+pbuf_flush(struct pbuf *pbuf)
+{
+ pbuf->packet->hdr.size = pbuf->cursor;
+ pbuf->cursor = 0;
+}
+
+void
+pbuf_write(struct pbuf *pbuf, const void *data, const size_t sz)
+{
+ assert(pbuf && pbuf->packet);
+ assert(sz <= sizeof(pbuf->packet->buf) && pbuf->cursor <= sizeof(pbuf->packet->buf) - sz);
+ memcpy(pbuf->packet->buf + pbuf->cursor, data, sz);
+ pbuf->cursor += sz;
+}
+
+void
+pbuf_write_str_len(struct pbuf *pbuf, const void *str, const uint16_t len)
+{
+ pbuf_write(pbuf, &len, sizeof(len));
+ pbuf_write(pbuf, str, len);
+}
+
+void
+pbuf_write_str(struct pbuf *pbuf, const char *str)
+{
+ assert(str);
+ pbuf_write_str_len(pbuf, str, strlen(str));
+}
+
+void
+pbuf_write_str_len_utf16(struct pbuf *pbuf, const void *str, const uint16_t len)
+{
+ struct pbuf_string u16;
+ u16.len = utf8_to_utf16(str, len, u16.data, sizeof(u16.data));
+ pbuf_write(pbuf, &u16.len, sizeof(u16.len));
+ pbuf_write(pbuf, u16.data, u16.len);
+}
+
+void
+pbuf_write_str_utf16(struct pbuf *pbuf, const char *str)
+{
+ assert(str);
+ pbuf_write_str_len_utf16(pbuf, str, strlen(str));
+}
+
+size_t
+pbuf_read_safe(struct pbuf *pbuf, void *data, const size_t data_sz, const size_t sz)
+{
+ assert(pbuf && pbuf->packet);
+ assert(sz <= sizeof(pbuf->packet->buf) && pbuf->cursor <= sizeof(pbuf->packet->buf) - sz);
+ const size_t to_copy = (sz > data_sz ? data_sz : sz);
+ memcpy(data, pbuf->packet->buf + pbuf->cursor, to_copy);
+ pbuf->cursor += sz;
+ return to_copy;
+}
+
+void
+pbuf_read(struct pbuf *pbuf, void *data, const size_t sz)
+{
+ pbuf_read_safe(pbuf, data, sz, sz);
+}
+
+void
+pbuf_read_str(struct pbuf *pbuf, struct pbuf_string *str)
+{
+ assert(str);
+ pbuf_read(pbuf, &str->len, sizeof(str->len));
+ str->len = pbuf_read_safe(pbuf, str->data, sizeof(str->data) - 1, str->len);
+ assert(str->len < sizeof(str->data));
+ str->data[str->len] = 0;
+}
+
+void
+pbuf_read_str_utf16(struct pbuf *pbuf, struct pbuf_string *str)
+{
+ assert(str);
+ struct pbuf_string u16;
+ pbuf_read(pbuf, &u16.len, sizeof(u16.len));
+ u16.len = pbuf_read_safe(pbuf, u16.data, sizeof(u16.data) - 1, u16.len);
+ assert(u16.len < sizeof(u16.data));
+ str->len = utf16_to_utf8(u16.data, u16.len, str->data, sizeof(str->data));
+}
diff --git a/src/packet.h b/src/packet.h
new file mode 100644
index 0000000..2219a4a
--- /dev/null
+++ b/src/packet.h
@@ -0,0 +1,535 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+// packet types, matching sw's strings
+enum {
+ // (send_eSUB_)?CMD_* ids crawled with find-client-packets.bash
+ PACKET_EVENT_SPAWN_BOX = 0x3111,
+ PACKET_EVENT_SCENE_DIRECTING = 0x3311,
+ PACKET_MONSTER_CLIENT_SPAWN = 0x7111,
+ PACKET_MAZE_ENTER_PARTY_RES = 0x4811,
+ PACKET_LOGIN_REQ = 0x0102,
+ PACKET_SERVER_LIST_REQ = 0x0302,
+ PACKET_SERVER_CONNECT_REQ = 0x0502,
+ PACKET_MOBILE_AUTH = 0x3302,
+ PACKET_ENTER_WAIT_CANCEL = 0x3502,
+ PACKET_CHARACTER_LIST_REQ = 0x1103,
+ PACKET_CREATE_CHARACTER_REQ = 0x0103,
+ PACKET_DELETE_CHARACTER_REQ = 0x0203,
+ PACKET_SELECT_CHARACTER_REQ = 0x1303,
+ PACKET_CHARACTER_CHANGE_SLOT = 0x0603,
+ PACKET_CHARACTER_INFO_REQ = 0x3103,
+ PACKET_ITEM_INVEN_INFO = 0x0108, // freezes
+ PACKET_ITEM_BANK_INFO = 0x1008,
+ PACKET_ITEM_MOVE = 0x0208,
+ PACKET_ITEM_COMBINE = 0x0308,
+ PACKET_ITEM_DIVIDE = 0x0408,
+ PACKET_ITEM_BREAK = 0x0508,
+ PACKET_ITEM_USE = 0x1108,
+ PACKET_ITEM_USE_SELECT = 0x6708,
+ PACKET_ITEM_MOVE_MONEY = 0x2408,
+ PACKET_ITEM_ADD_SLOT = 0x0f08,
+ PACKET_ITEM_LINE_UP = 0x2508,
+ PACKET_WORLD_DISTRICT_TRANSPORT_REQ = 0x4004,
+ PACKET_MAZE_CREATE_REQ = 0x4104,
+ PACKET_COMPLETE_MAZE_REQ = 0x2211,
+ PACKET_COMPLETE_MAZE_START_GAME = 0x2511,
+ PACKET_EXIT_MAZE_REQ = 0x2311,
+ PACKET_MAZE_SWITCH_NPC_CLICK_REQ = 0x7c11,
+ PACKET_MAZE_LUA_FUNCTION_REQ = 0x7e11,
+ PACKET_OPERATION_END_REQ = 0x6211,
+ PACKET_MAZE_INTERACTION_CLICK_REQ = 0x7711,
+ PACKET_MAZE_INTERACTION_MOTION = 0x7911,
+ PACKET_QUEST_MOVE_CHECK_REQ = 0x6511,
+ PACKET_CHECK_EVENT_SPAWN_BOX_REQ = 0x3511,
+ PACKET_MAZE_SECTOR_ERROR_FIX = 0x0311,
+ PACKET_PARTY_INVITE = 0x0112,
+ PACKET_PARTY_ACCEPT = 0x0212,
+ PACKET_PARTY_CHANGE_MASTER = 0x0312,
+ PACKET_PARTY_KICK_OUT = 0x0412,
+ PACKET_PARTY_LEAVE = 0x0512,
+ PACKET_SHOP_BUY = 0x0109,
+ PACKET_SHOP_SELL = 0x0209,
+ PACKET_SHOP_REPURCHASER_LIST = 0x0309,
+ PACKET_SHOP_REPURCHASER = 0x0409,
+ PACKET_SHOP_CASH_LOAD = 0x2009,
+ PACKET_SHOP_CASH_BUY = 0x2109,
+ PACKET_SHOP_CASH_SET = 0x2309,
+ PACKET_SHOP_CASH_SET_DEL = 0x2409,
+ PACKET_SHOP_CASH_GIFT = 0x2509,
+ PACKET_TRADE_REQ = 0x2a00,
+ PACKET_TRADE_ACCEPT = 0x2a00,
+ PACKET_TRADE_UPDATE_ITEM = 0x2a00,
+ PACKET_TRADE_UPDATE_MONEY = 0x2a00,
+ PACKET_TRADE_CHECK = 0x2a00,
+ PACKET_TRADE_CONFIRM = 0x2a00,
+ PACKET_TRADE_CANCEL = 0x2a00,
+ PACKET_PRIVATE_SHOP_START = 0x2a00,
+ PACKET_PRIVATE_SHOP_STATE = 0x2a00,
+ PACKET_PRIVATE_SHOP_ITEM = 0x2a00,
+ PACKET_PRIVATE_SHOP_SELECT = 0x2a00,
+ PACKET_OPTION_UPDATE = 0x0201,
+ PACKET_LOGIN_OPTION_UPDATE = 0x3202,
+ PACKET_CHAT_NORMAL = 0x0107,
+ PACKET_CHAT_WHISPER = 0x0207,
+ PACKET_CHAT_TRADE = 0x0307,
+ PACKET_CHAT_GM_COMMAND = 0x0607,
+ PACKET_ITEM_UPGRADE = 0x0218,
+ PACKET_ITEM_RESTORE = 0x2218,
+ PACKET_ITEM_UNSEAL = 0x2418,
+ PACKET_ITEM_USE_EFFECT = 0x2618,
+ PACKET_ITEM_EXCHANGE = 0x0318,
+ PACKET_ITEM_DISASSEMBLE = 0x0418,
+ PACKET_DROP_PICK_UP = 0x0214,
+ PACKET_ITEM_REPAIR = 0x0818,
+ PACKET_ITEM_REPAIR_NPC = 0x0918,
+ PACKET_ITEM_REPAIR_EQUIP = 0x1018,
+ PACKET_ITEM_REPAIR_ALL = 0x1118,
+ PACKET_ITEM_RENOVATE = 0x2718,
+ PACKET_ITEM_REFINE = 0x2918,
+ PACKET_ITEM_EVOLUTION = 0x1318,
+ PACKET_ITEM_AKASHIC_MAKE_EX = 0x2518,
+ PACKET_ITEM_AKASHIC_DISASSEMBLE = 0x1518,
+ PACKET_ITEM_AKASHIC_COMPOSE = 0x1718,
+ PACKET_ITEM_AKASHIC_COMPOSE_EX = 0x3318,
+ PACKET_ITEM_DISASSEMBLE_EX = 0x1818,
+ PACKET_QUEST_ACCEPT = 0x0315,
+ PACKET_QUEST_EPISODE_COMPLETE = 0x0515,
+ PACKET_QUEST_GIVE_UP = 0x0615,
+ PACKET_QUEST_EVENT_UPDATE = 0x0815,
+ PACKET_QUEST_HELPER = 0x0915,
+ PACKET_QUEST_FAIL = 0x1115,
+ PACKET_DAILY_MISSION_ACCEPT = 0x0224,
+ PACKET_DAILY_MISSION_HELPER = 0x0424,
+ PACKET_WEEKLY_MISSION_REWARD = 0x0332,
+ PACKET_WEEKLY_MISSION_REWARD_WEEK = 0x0432,
+ PACKET_CHARACTER_LOAD_TITLE = 0x2303,
+ PACKET_CHARACTER_GET_REWARD_SHARE_POINT = 0x6303,
+ PACKET_CHARACTER_UPDATE_TITLE = 0x2503,
+ PACKET_CHARACTER_FAVORITE_TITLE = 0x2a03,
+ PACKET_FRIEND_INVITE = 0x1119,
+ PACKET_FRIEND_INVITE_ACCEPT = 0x1319,
+ PACKET_FRIEND_DELETE = 0x1519,
+ PACKET_FRIEND_BLOCK_ADD = 0x2119,
+ PACKET_FRIEND_BLOCK_DEL = 0x2219,
+ PACKET_FRIEND_RECOMMAND_LIST = 0x5119,
+ PACKET_FRIEND_INFO = 0x3219,
+ PACKET_POST_SEND_LIST = 0x1020,
+ PACKET_POST_RECV_LIST = 0x0220,
+ PACKET_POST_SAVE_LIST = 0x0120,
+ PACKET_POST_READ = 0x0420,
+ PACKET_POST_ACCOUNT_READ = 0x1620,
+ PACKET_POST_SEND = 0x0320,
+ PACKET_POST_RECEIPT = 0x0520,
+ PACKET_POST_ACCOUNT_RECEIPT = 0x1720,
+ PACKET_POST_SENDBACK = 0x0820,
+ PACKET_POST_SEND_DEL = 0x0620,
+ PACKET_POST_RECV_DEL = 0x0720,
+ PACKET_POST_SAVE = 0x1820,
+ PACKET_POST_LIST_REFRESH = 0x1920,
+ PACKET_POST_RECEIPT_ALL = 0x2120,
+ PACKET_ENTER_SERVER_REQ = 0x1302,
+ PACKET_ENTER_GAMESERVER_REQ = 0x2103,
+ PACKET_ITEM_UPDATE_QUICKSLOT_CARD = 0x2708,
+ PACKET_ITEM_UPDATE_QUICKSLOT_ITEM = 0x2808,
+ PACKET_ITEM_MAZE_REWARD_ITEM = 0x4708,
+ PACKET_ITEM_APPEARANCE_EQUIP = 0x5208,
+ PACKET_ITEM_NAME_CHANGE = 0x5208,
+ PACKET_ITEM_UPDATE_CASH = 0x5308,
+ PACKET_ITEM_MAKE = 0x0118,
+ PACKET_ITEM_SOCKET_EQUIP = 0x0518,
+ PACKET_ITEM_SOCKET_ACTIVE = 0x0618,
+ PACKET_ITEM_SOCKET_DETACH = 0x0718,
+ PACKET_ITEM_BROACH_EQUIP = 0x2018,
+ PACKET_ITEM_BROACH_ACTIVE = 0x2118,
+ PACKET_ITEM_BROACH_COMPOSE = 0x2318,
+ PACKET_ITEM_BROACH_REMOVE = 0x2818,
+ PACKET_ITEM_SOCKET_EXCHANGE = 0x3018,
+ PACKET_ITEM_SOCKET_UPGRADE = 0x3118,
+ PACKET_ITEM_SOCKET_EXTRACT = 0x3218,
+ PACKET_ITEM_TITLE_CHANGE = 0x3718,
+ PACKET_ITEM_DYE = 0x3618,
+ PACKET_MODE_MAZE_MATCHING_ENTER = 0x0133,
+ PACKET_MODE_MAZE_MATCHING_EXIT = 0x0333,
+ PACKET_SOULMETRY_COMPLETE = 0x0521,
+ PACKET_VACCUM_CLICK_START = 0x0125,
+ PACKET_VACCUM_CLICK_CANCEL = 0x0225,
+ PACKET_LEAGUE_CREATE = 0x0122,
+ PACKET_LEAGUE_DELETE = 0x0222,
+ PACKET_LEAGUE_LIST = 0x0322,
+ PACKET_LEAGUE_INVITE = 0x1322,
+ PACKET_LEAGUE_INVITE_ACCEPT = 0x1422,
+ PACKET_LEAGUE_INVITE_REJECT = 0x1522,
+ PACKET_LEAGUE_WITHDRAW = 0x0922,
+ PACKET_LEAGUE_KICK = 0x0f22,
+ PACKET_LEAGUE_APPLICANT = 0x0522,
+ PACKET_LEAGUE_APPLICANT_ACCEPT = 0x1822,
+ PACKET_LEAGUE_APPLICANT_REJECT = 0x1922,
+ PACKET_LEAGUE_DELEGATE = 0x0a22,
+ PACKET_LEAGUE_BOARD = 0x0822,
+ PACKET_LEAGUE_SEARCH = 0x2022,
+ PACKET_LEAGUE_OVERLAP_NAME = 0x2122,
+ PACKET_LEAGUE_INFO = 0x0722,
+ PACKET_LEAGUE_AUTH_CHANGE = 0x3222,
+ PACKET_LEAGUE_NOTICE_CHANGE = 0x2922,
+ PACKET_LEAGUE_POSITION_NAME_CHANGE = 0x3322,
+ PACKET_LEAGUE_CARD_CHANGE = 0x3122,
+ PACKET_LEAGUE_SKILL_LEARN = 0x5322,
+ PACKET_LEAGUE_INVENTORY_MOVE = 0x5422,
+ PACKET_LEAGUE_NAME_CHANGE = 0x3022,
+ PACKET_LEAGUE_INVENTORY_INFO = 0x5622,
+ PACKET_LEAGUE_MEMBER_POSITION_CHANGE = 0x3922,
+ PACKET_LEAGUE_OPEN_OR_NOT = 0x4622,
+ PACKET_LEAGUE_RECRUIT_NOTICE = 0x4722,
+ PACKET_CHARATER_CHANGE_SERVER = 0x6003,
+ PACKET_CHARACTER_UPDATE_SPECIAL_OPTION_LIST = 0x4703,
+ PACKET_CHARACTER_CHECK_ENTER_MAZE = 0x4003,
+ PACKET_CHARACTER_PROFILE_PHOTO_FAVORITE = 0x0b03,
+ PACKET_CHARACTER_PROFILE_PHOTO_CAHNGE = 0x0c03,
+ PACKET_ACHIEVE_REWARD = 0x7403,
+ PACKET_DO_GESTURE = 0x0123,
+ PACKET_GESTURE_SLOT_UPDATE = 0x0323,
+ PACKET_OTHER_CHARACTER_INFO = 0x7503,
+ PACKET_CHARACTER_COMMUNITY = 0x7703,
+ PACKET_PARTY_CANCEL = 0x0812,
+ PACKET_PARTY_UPDATE_INFO = 0x0912,
+ PACKET_PARTY_MATCHING_ENTER = 0x3012,
+ PACKET_PARTY_MATCHING_EXIT = 0x3112,
+ PACKET_PARTY_MATCHING_CHECK = 0x3212,
+ PACKET_SYSTEM_XIGNCODE = 0x0301,
+ PACKET_PARTY_RECRUIT_LIST = 0x3612,
+ PACKET_PARTY_RECRUIT_ADD = 0x3712,
+ PACKET_PARTY_RECRUIT_APPLY = 0x3812,
+ PACKET_PARTY_RECRUIT_APPLY_ACCEPT = 0x3912,
+ PACKET_PARTY_RECRUIT_APPLY_REJECT = 0x3a12,
+ PACKET_PARTY_RECRUIT_DEL = 0x3c12,
+ PACKET_PARTY_RECRUIT_APPLY_LIST = 0x3d12,
+ PACKET_PARTY_AWAITER_LIST = 0x4212,
+ PACKET_PARTY_AWAITER_ADD = 0x4012,
+ PACKET_PARTY_AWAITER_DEL = 0x4112,
+ PACKET_PARTY_RECRUIT_APPLY_INFO = 0x4312,
+ PACKET_PARTY_AWAITER_INFO = 0x4412,
+ PACKET_FRIEND_RECRUIT_LIST = 0x4119,
+ PACKET_FRIEND_RECRUIT_ADD = 0x4219,
+ PACKET_FRIEND_RECRUIT_DEL = 0x4319,
+ PACKET_FRIEND_RECRUIT_INFO = 0x4419,
+ PACKET_FRIEND_FIND = 0x3319,
+ PACKET_MYROOM_ENTER_REQ = 0x1126,
+ PACKET_MYROOM_CREATE_REQ = 0x1026,
+ PACKET_MYROOM_EDIT_START = 0x0526,
+ PACKET_MYROOM_EDIT_END = 0x0626,
+ PACKET_MYROOM_RECOMMEND = 0x4026,
+ PACKET_MYROOM_FAVORITE = 0x4126,
+ PACKET_MYROOM_WRITE_BOARD = 0x4226,
+ PACKET_MYROOM_WRITE_INFO = 0x4326,
+ PACKET_MYROOM_BOARD_LIST = 0x4426,
+ PACKET_MYROOM_RANK_INFO = 0x4626,
+ PACKET_MYROOM_FAVORITE_INFO = 0x4726,
+ PACKET_MYROOM_RANK_REWARD = 0x4826,
+ PACKET_HELPER_SUMMON = 0x0227,
+ PACKET_HELPER_SUPPORT_INFO = 0x0527,
+ PACKET_HELPER_SUPPORT_REGISTER = 0x0627,
+ PACKET_HELPER_SUPPORT_REWARD = 0x0727,
+ PACKET_HELPER_SUPPORT_LIST = 0x0827,
+ PACKET_HELPER_SUPPORT_EQUIP = 0x0927,
+ PACKET_HELPER_EQUIP = 0x1027,
+ PACKET_HELPER_CHANGE_ORDER = 0x1027,
+ PACKET_HELPER_ALL_RELEASE = 0x1227,
+ PACKET_HELPER_AUTO_SUMMON = 0x1427,
+ PACKET_MYROOM_EXIT_REQ = 0x1326,
+ PACKET_MYROOM_SETUP = 0x1426,
+ PACKET_MYROOM_KICK_OUT = 0x1526,
+ PACKET_MYROOM_ITEM_ADD = 0x2026,
+ PACKET_MYROOM_ITEM_DEL = 0x2126,
+ PACKET_MYROOM_ITEM_USE = 0x2226,
+ PACKET_MYROOM_ITEM_RELEASE_USED = 0x2326,
+ PACKET_MYROOM_DOOR_STATE = 0x2626,
+ PACKET_MYROOM_POLLEN_ADD = 0x3126,
+ PACKET_MYROOM_POLLEN_CULTIVATION = 0x3226,
+ PACKET_MYROOM_POLLEN_HARVEST = 0x3326,
+ PACKET_MYROOM_POLLEN_HELP = 0x3426,
+ PACKET_MYROOM_POLLEN_ITEM_USE = 0x3526,
+ PACKET_MYROOM_PLLLEN_CANCEL = 0x3726,
+ PACKET_INFINITE_TOWER_ENTER_CHAPTER = 0x0228,
+ PACKET_INFINITE_TOWER_ENTER_NEXT_STAGE = 0x0328,
+ PACKET_SECOND_PASSWORD = 0x1703,
+ PACKET_TRADE_PASSWORD = 0x1803,
+ PACKET_CHARACTER_UPDATE_CUTSCENE = 0x1903,
+ PACKET_SHOP_GACHA = 0x2609,
+ PACKET_EXCHANGE_SEARCH = 0x012b,
+ PACKET_EXCHANGE_PRICE_HISTORY = 0x022b,
+ PACKET_EXCHANGE_INTEREST_LIST = 0x032b,
+ PACKET_EXCHANGE_INTEREST_ITEM = 0x042b,
+ PACKET_EXCHANGE_SELL_REGISTER = 0x052b,
+ PACKET_EXCHANGE_ITEM_BUY = 0x062b,
+ PACKET_EXCHANGE_ITEM_RECALL = 0x072b,
+ PACKET_EXCHANGE_MY_LIST = 0x082b,
+ PACKET_SOCIAL_ITEM_START = 0x012d,
+ PACKET_SOCIAL_ITEM_USE = 0x022d,
+ PACKET_SOCIAL_ITEM_STOP = 0x032d,
+ PACKET_RANKING_LIST = 0x012c,
+ PACKET_RANKING_REWARD = 0x022c,
+ PACKET_FORCE_INVITE = 0x012e,
+ PACKET_FORCE_ACCEPT = 0x022e,
+ PACKET_FORCE_CHANGE_MASTER = 0x032e,
+ PACKET_FORCE_KICK_OUT = 0x042e,
+ PACKET_FORCE_LEAVE = 0x052e,
+ PACKET_FORCE_CANCEL = 0x082e,
+ PACKET_FORCE_MATCHING_ENTER = 0x302e,
+ PACKET_FORCE_MATCHING_EXIT = 0x312e,
+ PACKET_EVENT_USE_COUPON_CODE = 0x212a,
+ PACKET_EVENT_NET_CAFE_ITEM_BUY = 0x2a2a,
+ PACKET_EVENT_WORLD_EVENT_INFO = 0x222a,
+ PACKET_EVENT_WORLD_EVENT_REGISTER = 0x232a,
+ PACKET_EVENT_WORLD_EVENT_REWARD = 0x242a,
+ PACKET_EVENT_WORLD_EVENT_DAILY_REWARD = 0x252a,
+ PACKET_EVENT_ROULETTE_USE = 0x292a,
+ PACKET_FORCE_MATCHING_CHECK = 0x322e,
+ PACKET_PROJECTILE_BT = 0x0112,
+ PACKET_CHAIN_BT = 0x0112,
+
+ // guesswork sends
+ PACKET_PROB_DISCONNECT = 0x1904,
+
+ // receives (manually figured out, the low byte is usually +1 of client REQ)
+ PACKET_LOGIN_RESULT = 0x0202,
+ PACKET_SERVER_LIST = 0x0402,
+ PACKET_ENTER_SERVER = 0x1102,
+ PACKET_ENTER_SERVER_RES = 0x1402,
+ PACKET_OPTION_LOAD = 0x3102,
+ PACKET_ENTER_WAIT_CHECK = 0x3402,
+ PACKET_SYSTEM_KEEP_ALIVE = 0x0501,
+ PACKET_SYSTEM_SERVER_OPTION_UPDATE = 0x0701,
+ PACKET_SYSTEM_EVENT = 0x0901,
+ PACKET_CHARACTER_LIST_RES = 0x1203,
+ PACKET_DELETE_CHARACTER_RES = 0x0303,
+ PACKET_SELECT_CHARACTER_RES = 0x1403,
+ PACKET_CHARACTER_ENTER_PROLOGUE = 0x1503,
+ PACKET_CHARACTER_ENTER_BATTLE_ZONE = 0x1603,
+ PACKET_CHARACTER_PLAY_CUTSCENE = 0x2003,
+
+ PACKET_ENTER_GAMESERVER_RES = 0x2203,
+ PACKET_CHARACTER_ADD_TITLE = 0x2403,
+ PACKET_CHARACTER_UPDATE_TITLE_BT = 0x2603,
+ PACKET_CHARACTER_CLEAR_TITLE = 0x2703,
+ PACKET_CHARACTER_UPDATE_OPEN_TITLE = 0x2903,
+ PACKET_CHARACTER_DB_LOAD_SYNC = 0x3003,
+ PACKET_CHARACTER_INFO_RES = 0x3203,
+ PACKET_CHARACTER_UPDATE_STAT_LIST = 0x3403,
+ PACKET_CHARACTER_UPDATE_ORIGIN_STAT = 0x5103,
+ PACKET_CHARACTER_CHECK_NAME = 0x5703,
+ PACKET_DAILY_MISSION_UPDATE = 0x3204,
+ PACKET_EVENT_ATTENDANCE_LOAD = 0x012a,
+ PACKET_EVENT_ROULETTE_MY_INFO = 0x282a,
+ PACKET_ITEM_UPDATE_CASH_RES = 0x3308,
+ PACKET_ITEM_AKASHIC_GETINFO_LOAD = 0x3518,
+ PACKET_INFINITE_TOWER_LOAD_INFO = 0x0128,
+ PACKET_CHARACTER_LOAD_DISTRICT_STATE = 0x6103,
+ PACKET_CHARACTER_LOAD_MAZE_STATE = 0x6203,
+ PACKET_MAZE_CLEAR_INFO = 0x6411,
+ PACKET_ENTER_MAZE_LIMIT_COUNT_LOAD = 0x4304,
+ PACKET_SOULMETRY_LIST = 0x0121,
+ PACKET_SOULMETRY_COMPLETE_LIST = 0x0221,
+ PACKET_ACHIEVE_SELECT = 0x7003,
+ PACKET_SHOP_BANNER_LOAD = 0x2809,
+ PACKET_SHOP_CASH_SET_LOAD = 0x2209,
+ PACKET_ITEM_APPEARANCE_LOAD = 0x5008,
+ PACKET_NPC_CREDIT_LOAD = 0x6503,
+ PACKET_SHOP_ITEM_LOAD = 0x1009,
+ PACKET_MYROOM_LOAD_INDEX = 0x1226,
+ PACKET_FRIEND_LOAD = 0x0119,
+ PACKET_FRIEND_LOAD_BLOCKLIST = 0x0219,
+ PACKET_DAILY_MISSION_LIST = 0x0124,
+ PACKET_HELPER_LIST_LOAD = 0x0127,
+ PACKET_ACHIEVE_UPDATE = 0x7103,
+ PACKET_WORLD_VERSION = 0x0404,
+ PACKET_EVENT_DAY_EVENT_BOOSTER_LIST = 0x202a,
+ PACKET_LEAGUE_INFO_ALERT = 0x4422,
+ PACKET_SHOP_CASH_BUY_COUNT_LOAD = 0x3009,
+ PACKET_SHOP_CASH_TAB_LOAD = 0x2909,
+ PACKET_IN_PC_INFO = 0x1104,
+ PACKET_OTHER_PC_INFOS = 0x2104,
+ PACKET_OTHER_INFOS_NPC = 0x2204,
+ PACKET_QUEST_LIST = 0x0215,
+ PACKET_GAME_WORLD_ENTER_RES = 0x0204,
+ PACKET_ITEM_CREATE = 0x0608,
+ PACKET_ITEM_MOVE_BT = 0xd08,
+ PACKET_GESTURES = 0x0223,
+
+ PACKET_SOME_PC_PACKET = 0x5104,
+
+ PACKET_UNKNOWN = 0x0601, // sent a lot
+ PACKET_AUTH_UNKNOWN1 = 0x0304, // freezes
+ PACKET_GAME_UNKNOWN1 = 0x0633, // sent after 2203 and 4703
+ PACKET_GAME_UNKNOWN2 = 0x6608,
+ PACKET_GAME_UNKNOWN3 = 0x6808,
+ PACKET_GAME_UNKNOWN4 = 0x1406,
+ PACKET_GAME_UNKNOWN5 = 0x0903,
+ PACKET_SKILLS = 0x7006,
+ PACKET_GAME_UNKNOWN10 = 0x4a08,
+ PACKET_GAME_UNKNOWN11 = 0x5708,
+ PACKET_GAME_UNKNOWN12 = 0x4808,
+ PACKET_GAME_UNKNOWN13 = 0x0604,
+ PACKET_GAME_UNKNOWN14 = 0x5317,
+ PACKET_GAME_UNKNOWN15 = 0x0705,
+ PACKET_GAME_UNKNOWN16 = 0x9203,
+ PACKET_POST_ACCOUNT_LIST = 0x1420,
+ PACKET_POST_ACCOUNT_RECV = 0x1520,
+ PACKET_DROP_INFOS = 0x0114,
+ PACKET_EVENT_ATTENDANCE_REWARD = 0x022a,
+ PACKET_QUEST_COMPLETE_LIST = 0x0115,
+ PACKET_GAME_UNKNOWN22 = 0x7551,
+ PACKET_GAME_UNKNOWN24 = 0x2106,
+ PACKET_BOOSTER_LIST_LOAD = 0x0129,
+ PACKET_CHANNEL_INFO = 0x01f1,
+ PACKET_GAME_UNKNOWN27 = 0x0205,
+ PACKET_BOOSTER_ADD = 0x0229,
+ PACKET_DROP_DELETE = 0x0314,
+ PACKET_SOULMETRY_ADD = 0x0321,
+ PACKET_BOOSTER_REMOVE = 0x0329,
+ PACKET_ATTENDANCE_CONTINUE_REWARD = 0x032a,
+ PACKET_GAME_UNKNOWN33 = 0x0405,
+ PACKET_HELPER_USER_STAT_UPDATE = 0x0427,
+ PACKET_NOTICE = 0x0507,
+ PACKET_EVENT_ATTENDANCE_PLAY_TIME_INIT = 0x052a,
+ PACKET_GAME_UNKNOWN37 = 0x0605,
+ PACKET_SOCIAL_ITEM_INFOS = 0x062d,
+ PACKET_GAME_UNKNOWN39 = 0x0706,
+ PACKET_QUEST_UPDATE = 0x0715,
+ PACKET_GAME_UNKNOWN41 = 0x0803,
+ PACKET_WORLD_WARP_RES = 0x0804,
+ PACKET_GAME_UNKNOWN43 = 0x0805,
+ PACKET_WORLD_WARP_ENABLE_RES = 0x0904,
+ PACKET_CHARACTER_ACTION_REQ = 0x0806,
+ PACKET_CHARACTER_ACTION_RES = 0x0906,
+ PACKET_GAME_UNKNOWN46 = 0x0c05,
+ PACKET_GAME_UNKNOWN47 = 0x0d05,
+ PACKET_WORLD_WARP_MAZE_RES = 0x0e04,
+ PACKET_ITEM_OPEN_SLOT_INFO = 0x0e08,
+ PACKET_GAME_UNKNOWN50 = 0x0f05,
+ PACKET_GAME_UNKNOWN51 = 0x1006,
+ PACKET_GAME_UNKNOWN52 = 0x1105,
+ PACKET_GAME_UNKNOWN53 = 0x1117,
+ PACKET_OUT_INFO_PC = 0x1204,
+ PACKET_ITEM_REDUCE = 0x1208,
+ PACKET_GAME_UNKNOWN56 = 0x1217,
+ PACKET_ITEM_ENDURANCE = 0x1218,
+ PACKET_GAME_UNKNOWN58 = 0x1305,
+ PACKET_GAME_UNKNOWN59 = 0x1306,
+ PACKET_GAME_UNKNOWN60 = 0x1308,
+ PACKET_VACCUM_INFOS = 0x1325,
+ PACKET_GAME_UNKNOWN62 = 0x1405,
+ PACKET_GAME_UNKNOWN63 = 0x1408,
+ PACKET_IN_INFO_MONSTER = 0x1504,
+ PACKET_GAME_UNKNOWN65 = 0x1506,
+ PACKET_OUT_INFO_MONSTER = 0x1604,
+ PACKET_MOVE_TRANSPORT_TAKE = 0x1605,
+ PACKET_MOVE_TRANSPORT_OFF = 0x1705,
+ PACKET_RECEIVE_ZN = 0x2008,
+ PACKET_GAME_UNKNOWN70 = 0x2108,
+ PACKET_GAME_UNKNOWN71 = 0x2111,
+ PACKET_GAME_UNKNOWN72 = 0x2217,
+ PACKET_OTHER_INFOS_MONSTER = 0x2304,
+ PACKET_GAME_UNKNOWN74 = 0x2404, // load quickslot
+ PACKET_GAME_UNKNOWN75 = 0x2608, // ^
+ PACKET_GAME_UNKNOWN76 = 0x3206, // ^ some of these
+ PACKET_GAME_UNKNOWN77 = 0x3208,
+ PACKET_MOVE_LOOP_MOTION_END_BT = 0x3305,
+ PACKET_GAME_UNKNOWN79 = 0x3317,
+ PACKET_MOVE_ATTACED_BT = 0x3405,
+ PACKET_MOVE_ATTACED_END_BT = 0x3505,
+ PACKET_GAME_UNKNOWN82 = 0x3603,
+ PACKET_RECEIVE_XP = 0x3703,
+ PACKET_ENTER_MAZE_LIMIT_COUNT_RESET = 0x4404,
+ PACKET_GAME_UNKNOWN85 = 0x4606,
+ PACKET_GAME_UNKNOWN86 = 0x4906,
+ PACKET_GAME_UNKNOWN87 = 0x5204,
+ PACKET_GAME_UNKNOWN88 = 0x5211, // bgm stuff?
+ PACKET_GAME_UNKNOWN89 = 0x5311,
+ PACKET_GAME_UNKNOWN90 = 0x5411,
+ PACKET_GAME_UNKNOWN91 = 0x5508,
+ PACKET_GAME_UNKNOWN92 = 0x5511, // portal stuff?
+ PACKET_ITEM_BROACH_LOAD = 0x5608,
+ PACKET_GAME_UNKNOWN94 = 0x5611,
+ PACKET_GAME_UNKNOWN95 = 0x5711,
+ PACKET_CHARACTER_UPDATE_SHARE_POINT = 0x5803,
+ PACKET_GAME_UNKNOWN97 = 0x5811,
+ PACKET_ITEM_SOCKET_UPDATE = 0x6108,
+ PACKET_ITEM_BROACH_UPDATE = 0x6208,
+ PACKET_CHARACTER_FP_UPDATE = 0x6403,
+ PACKET_GAME_UNKNOWN101 = 0x6611, // event box stuff
+ PACKET_GAME_UNKNOWN102 = 0x6811,
+ PACKET_GAME_UNKNOWN103 = 0x7011,
+ PACKET_GAME_UNKNOWN104 = 0x7306,
+ PACKET_GAME_UNKNOWN105 = 0x7511, // end maze / receive reward
+ PACKET_GAME_UNKNOWN106 = 0x7906,
+ PACKET_GAME_UNKNOWN107 = 0x7e06,
+ PACKET_GAME_UNKNOWN108 = 0x7f11, // event box stuff
+ PACKET_GAME_UNKNOWN109 = 0x2205,
+ PACKET_CHARACTER_MOVE = 0x105,
+ PACKET_CHARACTER_MOVE_RES = 0x205,
+ PACKET_CHARACTER_STOP_MOVE = 0x305,
+ PACKET_CHARACTER_STOP_MOVE_RES = 0x405,
+ PACKET_CHARACTER_JUMP = 0x0505,
+ PACKET_CHARACTER_JUMP_RES = 0x0605,
+
+ // 0x8001 and above make client try to execute a script?
+};
+
+union packet {
+ struct {
+ uint16_t salt, size;
+ uint8_t unknown;
+ uint16_t type;
+ } __attribute__((packed)) hdr;
+ uint8_t buf[4096 * 4];
+};
+
+struct pbuf {
+ union packet *packet;
+ size_t cursor;
+};
+
+struct pbuf_string {
+ char data[256];
+ uint16_t len;
+};
+
+bool
+packet_verify(const union packet *packet);
+
+void
+packet_crypt(union packet *packet);
+
+void
+pbuf_flush(struct pbuf *pbuf);
+
+void
+pbuf_write(struct pbuf *pbuf, const void *data, const size_t sz);
+
+void
+pbuf_write_str_len(struct pbuf *pbuf, const void *str, const uint16_t len);
+
+void
+pbuf_write_str(struct pbuf *pbuf, const char *str);
+
+void
+pbuf_write_str_len_utf16(struct pbuf *pbuf, const void *str, const uint16_t len);
+
+void
+pbuf_write_str_utf16(struct pbuf *pbuf, const char *str);
+
+size_t
+pbuf_read_safe(struct pbuf *pbuf, void *data, const size_t data_sz, const size_t sz);
+
+void
+pbuf_read(struct pbuf *pbuf, void *data, const size_t sz);
+
+void
+pbuf_read_str(struct pbuf *pbuf, struct pbuf_string *str);
+
+void
+pbuf_read_str_utf16(struct pbuf *pbuf, struct pbuf_string *str);
diff --git a/src/sw-crypt.c b/src/sw-crypt.c
new file mode 100644
index 0000000..04c9d0f
--- /dev/null
+++ b/src/sw-crypt.c
@@ -0,0 +1,25 @@
+#include "packet.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <err.h>
+
+int
+main(void)
+{
+ setvbuf(stdout, NULL, _IONBF, 0);
+
+ union packet packet;
+ for (size_t psz; (psz = fread(packet.buf, 1, sizeof(packet.hdr), stdin)) > 0;) {
+ if (!packet_verify(&packet))
+ errx(EXIT_FAILURE, "invalid packet");
+
+ psz += fread(packet.buf + sizeof(packet.hdr), 1, packet.hdr.size - sizeof(packet.hdr), stdin);
+ if (psz != packet.hdr.size)
+ errx(EXIT_FAILURE, "packet size doesn't match, got %zu, expected %u", psz, packet.hdr.size);
+
+ packet_crypt(&packet);
+ fwrite(packet.buf, 1, packet.hdr.size, stdout);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/xor.c b/src/xor.c
new file mode 100644
index 0000000..15411ab
--- /dev/null
+++ b/src/xor.c
@@ -0,0 +1,20 @@
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <err.h>
+
+int
+main(int argc, char *argv[])
+{
+ if (argc < 2)
+ errx(EXIT_FAILURE, "usage: %s hex ... < input > output", argv[0]);
+
+ uint8_t buf[4096];
+ for (size_t ret, c = 0; (ret = fread(buf, 1, sizeof(buf), stdin)) > 0;) {
+ for (size_t i = 0; i < ret; ++i, c = (c + 1) % (argc - 1))
+ buf[i] ^= strtoull(argv[c + 1], 0, 16);
+ fwrite(buf, 1, ret, stdout);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/tools/find-client-packets.bash b/tools/find-client-packets.bash
new file mode 100644
index 0000000..34882c8
--- /dev/null
+++ b/tools/find-client-packets.bash
@@ -0,0 +1,31 @@
+#!/bin/bash
+# Crawl soulworker for packet ids
+
+base=0x401200
+grep -obUaP '(send_)?(eSUB_)?CMD_.*?\x00' "$@" | while IFS=':' read -r off name; do
+ name="${name/*CMD/PACKET}"
+ name="$(awk '{print $1}' <<<"$name")"
+ off=$(rax2 -e "$off+$base")
+ off=${off//0x/}
+ while [[ "${#off}" -lt 8 ]]; do off="0$off"; done
+ rafind2 -nx "68$off" "$@" | while read -r off; do
+ off=$(rax2 "$off")
+ head -c $off "$@" | LC_ALL=C tac -rs $'.' | grep -obUaP -m1 '.\x6a.\x6a' | while IFS=':' read -r off2 nul; do
+ lo=$(xxd -s $((off-off2-3)) -l 1 -g 1 -ps "$@")
+ hi=$(xxd -s $((off-off2-1)) -l 1 -g 1 -ps "$@")
+ if [[ "$lo$hi" == '0001' ]] || [[ "$lo$hi" == '0000' ]]; then
+ break
+ fi
+ [[ "$name" == "PACKET_SHOP_CASH_GIFT" ]] && lo=25
+ [[ "$name" == "PACKET_ITEM_RESTORE" ]] && lo=22
+ [[ "$name" == "PACKET_ITEM_UNSEAL" ]] && lo=24
+ [[ "$name" == "PACKET_ITEM_USE_EFFECT" ]] && lo=26
+ [[ "$name" == "PACKET_ITEM_APPEARANCE_EQUIP" ]] && lo=52
+ [[ "$name" == "PACKET_LEAGUE_DELEGATE" ]] && lo=0a
+ [[ "$name" == "PACKET_HELPER_SUPPORT_INFO" ]] && lo=05
+ printf "%s = 0x%s%s,\n" "$name" "$lo" "$hi"
+ break
+ done
+ break
+ done
+done
diff --git a/tools/vbatch2c.py b/tools/vbatch2c.py
new file mode 100644
index 0000000..fdfbf92
--- /dev/null
+++ b/tools/vbatch2c.py
@@ -0,0 +1,36 @@
+import sys, os, operator, xml.etree.ElementTree
+
+zones = []
+d = os.fsencode(sys.argv[1])
+for f in os.listdir(d):
+ fname = os.fsdecode(f)
+ if not fname.endswith('.vbatch'):
+ continue
+
+ e = xml.etree.ElementTree.parse(fname).getroot()
+ s = e.find('Batchs').find('VStartEventBox')
+ if s:
+ name = os.path.splitext(fname)[0]
+ name = name if name.endswith('TUTORIAL') or name.endswith('GOLDENCITADEL') or name.endswith('STEELGRAVE') else name.split('_', 1)[-1]
+ zone = {
+ 'id': s.find('m_ID').get('value')[0:5],
+ 'name': name,
+ 'tl': s.find('m_vPosTopLeft').get('value'),
+ 'br': s.find('m_vPosBottomRight').get('value')
+ }
+ zones.append(zone)
+
+zones = sorted(zones, key=operator.itemgetter('name'))
+
+print('#pragma once\n')
+
+print('''static const struct {
+ float box[2][3];
+ const char *name;
+ uint32_t id;
+} ZONES[] = {''')
+
+for z in zones:
+ print(' {{ .id = {}, .name = "{}", .box = {{ {{ {} }}, {{ {} }} }} }},'.format(z['id'], z['name'], z['tl'], z['br']))
+
+print('};\n')