/*
 * tngp/tngs.c
 *
 * Copyright (C) 2022 bzt
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * @brief Main function for the TirNanoG Server
 * https://tirnanog.codeberg.page
 *
 */

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <signal.h>
#ifdef __WIN32__
#include <ws2tcpip.h>
#define FMT "I64u"
#define CLIFLAG "/"
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#define FMT "lu"
#define CLIFLAG "-"
#endif
#include <pthread.h>
#include "SDL_endian.h"
#define NETQ_IMPLEMENTATION
#define NETQ_SEND main_udp_sendto
#define NETQ_NO_PTHREAD
#include "netq.h"
#include "zlib.h"
#include "files.h"
#include "tng.h"
#define SDL_Texture int
#define SDL_Point int
#define SDL_Rect int
#include "world.h"
#include "script.h"
#include "version.h"
#include "tngs.h"
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_STDIO
#define STBI_NO_LINEAR
#define STBI_NO_HDR
#define STBI_ONLY_PNG
#include "stb_image.h"
#include "mbedtls/common.h"
#include "mbedtls/platform.h"
#include "mbedtls/platform_util.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/x509.h"
#include "mbedtls/ssl.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/error.h"
#include "mbedtls/debug.h"
/*
 * We're using hardcoded, self-signed certificate, because:
 * 1. we only want to protect against packet sniffers, so we care about strong encryption, but not about authentication
 * 2. admin interface is configured and used by the same person (in a protected LAN, but more likely on the very same machine)
 * 3. otherwise we use a custom protocol with custom authentication, not HTTPS, so only our own clients can connect anyway
 * 4. finally (but not least), I highly doubt anyone would willingly pay money to any CA just to run a Free and Open Source game
 */
#include "certs.h"

/*
TirNanoG Server Multithreaded Architecture
==========================================

The main_run() Procedure
------------------------

    (START)
       |
       v
    [load world from game.tng]
       |
       v
    [signal handler main_stop()]
       |
       v
    / run in background? /
       | N           Y \
       v                v
    [main_start()]   / forked child? /
        \             / N       | Y
         \           /          |
          \         /           v
           \       /        [daemonize by forking again]
            \     /             |
             v   v              v
             (END)           [main_start()]

The Networking Servers
----------------------

    main_start()                           main_ssl_srv()
       |                                   ____ |
       v                                  /    \|
    [main_bind()]                        |      v
       |                                 |   / is there a new TCP connection with SSL/TLS? /
       v                                 |  /  if so, create a thread to handle           /
    [create thread]                      |      | N              | Y
       |       \                          \____/                 v
       |        v                                          { main_ssl_con() }
   ___ |     { main_ssl_srv() }
  /   \|
 |     v
 |  / is there an UDP message?          /
 |     | N             | Y
 |     |               v
 |     |         [main_udp_push()]
 |     |               |
 |     v               v
 |  [clean up timed out clients]
 |     |
 |     v
 |  / is there any UDP pending?         /
 | /  if so, create a thread to handle /
 |     | N                          \
 |     v                             v
 |  [update internal game state]   { main_udp_pop() }
 |  [sleep a bit]
  \___/

main_ssl_con()  - handles new TirNanoG client connections and the admin REST API requests over SSL/TLS.
main_udp_push() - handles (the unencrypted) UDP Hello otherwise decrypts and pushes the message to one of the network queues.
main_udp_pop()  - is responsible for parsing the messages and keeping the game state in sync between the clients.
main_udp_send() - sends (an encrypted) message to the specified client.

main_start()'s loop runs once in every 1/1000th second. By removing the sleep it will go full throughput, but will eat up cpu.

Client side counterparts in client.c:
main_ssl_con():  client_connect(), client_register(), client_login()
main_udp_send(): client_recv() (server to client communication)
main_udp_pop():  client_send() (client to server communication)
*/

typedef struct {
    mbedtls_net_context client_fd;
    uint8_t peer_addr[16], peer_fam;
} ssl_conn_t;

/* command line arguments */
int verbose = 0, foreground = 0, maxplayers = 16, noreg = 0;
char *main_fn = NULL, *logfile = NULL, *savefile = NULL, *adminip = NULL, *bindip = NULL, *port = "4433";

/* global variables */
int udpsock = 0, tui = 0;
char server_ip[INET6_ADDRSTRLEN];
const char *main_me = "tngs";
uint8_t admin_addr[16];
int saveival = 3600, admin_addrlen = 0, server_port = 0;
uint64_t saved = 0;
struct timeval started, now;
volatile uint32_t main_tick = 0, dialog_stop = 0;
char *tr_url = NULL, *tr_method = NULL, *tr_par = NULL, *tr_res = NULL, *tr_hdr = NULL, *tr_cookie = NULL, *tr_curl = NULL;
int tr_bad = 0;
FILE *logf = NULL;
pthread_t ssl_thread = 0;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_ssl_config conf;
mbedtls_x509_crt srvcert;
mbedtls_pk_context pkey;
mbedtls_net_context listen_fd;

/* our own functions */
int mbedtls_hardware_poll( void *data, unsigned char *output, size_t len, size_t *olen );
char *json_get(const char *jsonstr, char *key);
void map_init();
void map_tile2map(int srcx, int srcy, int *dstx, int *dsty);
void map_map2tile(int srcx, int srcy, int *dstx, int *dsty);
int map_srv_neighbor(int map1, int map2);
int map_srv_dist(world_entity_t *this, world_entity_t *ent);
int nanosleep(const struct timespec *rqtp, struct timespec *rmtp);
int main_udp_send(int idx, int type, uint8_t *buf, int len);
int main_run();

/* we don't have any audio, nor user input or any interface at all, but scripts may call or need these */
int cutscn_idx = -1, client_eqchange = 0; world_scene_t game_scene = { 0 }, cutscn_scene = { 0 };
world_inv_t hud_belt[32];
void media_playmus(world_asset_t *once, world_asset_t *loop) { (void)once; (void)loop; }
void media_playspc(int idx, int x, int y) { (void)idx; (void)x; (void)y; }
void media_playsnd(int idx, int x, int y) { (void)idx; (void)x; (void)y; }
void alert_init(int idx) { (void)idx; }
void cutscn_init(int idx, int *flag) { (void)idx; (void)flag; }
int  cutscn_loadmap(int map, int x, int y, int daytime) { (void)map; (void)x; (void)y; (void)daytime; return 1; }
void cutscn_placenpc(int idx, int x, int y, int d) { (void)idx; (void)x; (void)y; (void)d; }
void chooser_init(int idx, int *flag, int *regs) { (void)idx; (void)flag; (void)regs; }
void dialog_init(int idx, int *flag, int *ret) { (void)idx; (void)flag; (void)ret; }
void hud_market(int npc) { (void)npc; }
void hud_craft(int craft) { (void)craft; }
void game_loadmap(int map_id) { (void)map_id; }
void main_addquest(world_entity_t *ent, int idx, int complete) { (void)ent; (void)idx; (void)complete; }
void main_freetranslator();
int  client_send(int type, uint8_t *buf, int len) { (void)type; (void)buf; (void)len; return 0; }

/**
 * Get elapsed game time in 1/1000th secs
 */
void main_gettick()
{
    gettimeofday(&now, NULL);
    main_tick = (now.tv_sec-started.tv_sec)*1000+(now.tv_usec-started.tv_usec)/1000;
}

/**
 * Memory allocation-free, in-place URL decoder
 */
#define isx(a) ((a >= '0' && a <= '9') || (a >= 'a' && a <= 'f') || (a >= 'A' && a <= 'F'))
char *urldecode(char *s)
{
    char a,b,*d=s,*o=s;
    while((s-o)<256 && *s && *s!='&' && *s!=' ' && *s!='\n') {
        if(*s=='%' && s[1] && s[2] && isx(s[1]) && isx(s[2])) {
            a=s[1]; b=s[2];
            if(a>='a'&&a<='f') a-='a'-10; else if(a>='A'&&a<='F') a-='A'-10; else a-='0';
            if(b>='a'&&b<='f') b-='a'-10; else if(b>='A'&&b<='F') b-='A'-10; else b-='0';
            *d++=((a<<4)|b); s+=2;
        } else if(*s=='+') { *d++=' '; } else { *d++=*s; }
        s++;
    }
    if(*s=='&'||*s==' ') s++;
    *d=0;
    return s;
}

/**
 * Encode a string for URL. Returns a new malloc'd string
 */
char *urlencode(char *s)
{
    char *d, *ret = malloc(strlen(s) * 3);
    if(ret) {
        d = ret;
        while(*s) {
            if(*s == ' ') {
                *d++ = '+';
            } else if(*s != '/' && *s != ':' && *s != '?' && *s != '%' && *s != '=' &&
                *s != '&' && *s != '\'' && *s != '\"' && *s != '*' && *s > ' ' && *s != 127) {
                    *d++ = *s;
            } else {
                sprintf(d, "%%%02X", *s);
                d += 3;
            }
            s++;
        }
        *d=0;
    }
    return ret;
}

/**
 * Print program version and copyright
 */
void main_hdr()
{
    printf("\r\nTirNanoG Server v%s (build %u) by bzt Copyright (C) 2021 - 2022 GPLv3+\r\n\r\n", tngpver, BUILD);
}

/**
 * Log messages
 */
void main_log(int level, const char *fmt, ...)
{
    struct tm tm = { 0 };
    time_t t;
    FILE *f = foreground ? stdout : logf;
    __builtin_va_list args;
    __builtin_va_start(args, fmt);
    if(level > verbose || !f) return;
    t = now.tv_sec;
    mbedtls_platform_gmtime_r(&t, &tm);
    fprintf(f, "\r%04u-%02u-%02uT%02u:%02u:%02u %u: ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
        tm.tm_hour, tm.tm_min, tm.tm_sec, getpid());
    vfprintf(f, fmt, args);
    fprintf(f, "\n");
    fflush(f);
}

/**
 * Allocate memory with error handler
 */
void *main_alloc(size_t size)
{
    void *tmp = malloc(size);
    if(!tmp) { main_log(0, "unable to allocate memory"); exit(1); }
    memset(tmp, 0, size);
    return tmp;
}

/**
 * Reallocate memory with error handler
 */
void *main_realloc(void *ptr, size_t size)
{
    void *tmp = realloc(ptr, size);
    if(!tmp) { main_log(0, "unable to allocate memory"); exit(1); }
    return tmp;
}

/**
 * Save game
 */
int main_savegame()
{
    int ret;

    if(savefile) {
        if((ret = world_savestate(savefile, NULL, 0, 0))) {
            main_log(0, "saving world state");
            saved = now.tv_sec;
        }
    } else ret = 0;
    return ret;
}

/**
 * Parse a translator configuration in JSON
 */
void main_gettranslator(char *fn)
{
    FILE *f;
    uint64_t siz;
    char *buf, *tmp, key[16];
    int i, j, l = 0;

    if(!fn || !*fn) return;
    siz = files_size(fn);
    f = siz > 0 ? files_open(fn, "rb") : NULL;
    if(!f) {
err:    printf("tngs: unable to open translator configuration '%s'\r\n", fn);
        if(tr_url) free(tr_url);
        tr_url = NULL;
        return;
    }
    buf = (char*)main_alloc(siz);
    if(!fread(buf, 1, siz, f)) { fclose(f); goto err; }
    fclose(f);
    tr_url = json_get(buf, "URL");
    tr_method = json_get(buf, "Method");
    tr_par = json_get(buf, "Param");
    tr_res = json_get(buf, "Result");
    tr_curl = json_get(buf, "CookieURL");
    tr_cookie = NULL;
    for(i = l = 0; i < 100; i++) {
        sprintf(key, "Cookies.%u", i);
        tmp = json_get(buf, key);
        if(tmp) {
            j = strlen(tmp);
            tr_cookie = (char*)realloc(tr_cookie, l + j + 3);
            if(tr_cookie) { if(l) { memcpy(tr_cookie + l, "; ", 2); l += 2; } memcpy(tr_cookie + l, tmp, j + 1); l += j; }
            else l = 0;
            free(tmp);
        } else break;
    }
    tr_hdr = NULL;
    for(i = 0; i < 100; i++) {
        sprintf(key, "Headers.%u", i);
        tmp = json_get(buf, key);
        if(tmp) {
            j = strlen(tmp);
            tr_hdr = (char*)realloc(tr_hdr, l + j + 3);
            if(tr_hdr) { memcpy(tr_hdr + l, tmp, j); memcpy(tr_hdr + l + j, "\r\n", 3); l += j + 2; }
            else l = 0;
            free(tmp);
        } else break;
    }
    free(buf);
}

/**
 * Free translator resources
 */
void main_freetranslator()
{
    if(tr_url) { free(tr_url); tr_url = NULL; }
    if(tr_method) { free(tr_method); tr_method = NULL; }
    if(tr_par) { free(tr_par); tr_par = NULL; }
    if(tr_res) { free(tr_res); tr_res = NULL; }
    if(tr_hdr) { free(tr_hdr); tr_hdr = NULL; }
    if(tr_cookie) { free(tr_cookie); tr_cookie = NULL; }
    if(tr_curl) { free(tr_curl); tr_curl = NULL; }
}

/**
 * Parse URL for query parameters. Replaces
 * {from} - two letter language code, translating from
 * {FROM} - three letter language code, translating from
 * {to} - two letter language cude, translating to
 * {TO} - three letter language cude, translating to
 * {msg} - the url encoded message
 * {MSG} - the raw message
 */
char *main_tr_parse(char *param, char *from, char *to, char *msg)
{
    char *iso639[]={"aa:aar","ab:abk","af:afr","ak:aka","am:amh","ar:ara","an:arg","as:asm","av:ava","ae:ave","ay:aym","az:aze"
        "ba:bak","bm:bam","be:bel","bn:ben","bi:bis","bo:bod","bs:bos","br:bre","bg:bul","ca:cat","cs:ces","ch:cha","ce:che"
        "cu:chu","cv:chv","kw:cor","co:cos","cr:cre","cy:cym","da:dan","de:deu","dv:div","dz:dzo","el:ell","en:eng","eo:epo"
        "et:est","eu:eus","ee:ewe","fo:fao","fa:fas","fj:fij","fi:fin","fr:fra","fy:fry","ff:ful","gd:gla","ga:gle","gl:glg"
        "gv:glv","gn:grn","gu:guj","ht:hat","ha:hau","he:heb","hz:her","hi:hin","ho:hmo","hr:hrv","hu:hun","hy:hye","ig:ibo"
        "io:ido","ii:iii","iu:iku","ie:ile","ia:ina","id:ind","ik:ipk","is:isl","it:ita","jv:jav","ja:jpn","kl:kal","kn:kan"
        "ks:kas","ka:kat","kr:kau","kk:kaz","km:khm","ki:kik","rw:kin","ky:kir","kv:kom","kg:kon","ko:kor","kj:kua","ku:kur"
        "lo:lao","la:lat","lv:lav","li:lim","ln:lin","lt:lit","lb:ltz","lu:lub","lg:lug","mh:mah","ml:mal","mr:mar","mk:mkd"
        "mg:mlg","mt:mlt","mn:mon","mi:mri","ms:msa","my:mya","na:nau","nv:nav","nr:nbl","nd:nde","ng:ndo","ne:nep","nl:nld"
        "nn:nno","nb:nob","no:nor","ny:nya","oc:oci","oj:oji","or:ori","om:orm","os:oss","pa:pan","pi:pli","pl:pol","pt:por"
        "ps:pus","qu:que","rm:roh","ro:ron","rn:run","ru:rus","sg:sag","sa:san","si:sin","sk:slk","sl:slv","se:sme","sm:smo"
        "sn:sna","sd:snd","so:som","st:sot","es:spa","sq:sqi","sc:srd","sr:srp","ss:ssw","su:sun","sw:swa","sv:swe","ty:tah"
        "ta:tam","tt:tat","te:tel","tg:tgk","tl:tgl","th:tha","ti:tir","to:ton","tn:tsn","ts:tso","tk:tuk","tr:tur","tw:twi"
        "ug:uig","uk:ukr","ur:urd","uz:uzb","ve:ven","vi:vie","vo:vol","wa:wln","wo:wol","xh:xho","yi:yid","yo:yor","za:zha"
        "zh:zho","zu:zul", NULL};
    char *ret, *um, *s, *d;
    int l, i, j;

    if(!param) return NULL;
    if(!from || !to || !msg || !*msg) {
err:    ret = (char*)malloc(strlen(param) + 1);
        if(ret) strcpy(ret, param);
        return ret;
    }
    um = urlencode(msg);
    if(!um) goto err;
    l = strlen(um); j = strlen(msg);
    ret = (char*)malloc(strlen(param) + l);
    if(!ret) { free(um); return NULL; }
    for(s = param, d = ret; *s; s++) {
        if(*s == '{') {
            s++;
            switch(*s) {
                case 'f': *d++ = from[0]; *d++ = from[1]; break;
                case 'F':
                    for(i = 0; iso639[i]; i++)
                        if(iso639[i][0] == from[0] && iso639[i][1] == from[1]) {
                            *d++ = iso639[i][3]; *d++ = iso639[i][4]; *d++ = iso639[i][5]; break;
                        }
                break;
                case 't': *d++ = to[0]; *d++ = to[1]; break;
                case 'T':
                    for(i = 0; iso639[i]; i++)
                        if(iso639[i][0] == to[0] && iso639[i][1] == to[1]) {
                            *d++ = iso639[i][3]; *d++ = iso639[i][4]; *d++ = iso639[i][5]; break;
                        }
                break;
                case 'm': memcpy(d, um, l); d += l; break;
                case 'M': memcpy(d, msg, j); d += j; break;
            }
            while(*s && *s != '}') s++;
            if(!*s) break;
        } else
            *d++ = *s;
    }
    free(um);
    return ret;
}

/**
 * Translate a message
 */
void main_translate(int idx, int num, uint8_t *buf, int len)
{
    files_t *f;
    uint8_t msg[1028];
    char *langs, **translated, *url, *par, *cookie = NULL;
    int j, k, l, dl;

    if(num < 1 || !buf || len < 1) return;
    langs = (char*)malloc(num * 2);
    if(!langs) return;
    memset(langs, 0, num * 2);
    translated = (char**)malloc(num * sizeof(char*));
    if(!translated) { free(langs); return; }
    memset(translated, 0, num * sizeof(char*));
    j = SDL_SwapLE32(idx); memcpy(msg, &j, 4); buf[len] = 0;

    /* get which languages we need to translate to */
    for(j = dl = 0; j < world.numplayers && dl < num; j++)
        if(idx != j && !world.players[j].banned && !world.players[j].logoutd && world.players[j].remote_port &&
          memcmp(world.players[idx].lang, world.players[j].lang, 2) && map_srv_dist(&world.players[idx].data, &world.players[j].data)) {
            for(l = 0; l < dl && memcmp(langs + 2 * l, world.players[j].lang, 2); l++);
            if(l >= dl) { memcpy(langs + 2 * dl, world.players[j].lang, 2); dl++; }
        }

    /* call the configured webtranslator to translate message, preferably in batch mode */
    if(tr_url) {
        if(tr_cookie && *tr_cookie) {
            cookie = (char*)malloc(strlen(tr_cookie) + 1);
            if(cookie) strcpy(cookie, tr_cookie);
        }
        if(tr_curl) {
            f = files_ssl_open(tr_curl, "GET", NULL, tr_hdr, &cookie);
            if(f) files_ssl_close(f);
        }
    }
    for(j = 0; j < dl; j++) {
        /* translate `buf` from `world.players[idx].lang` to `langs[j*2],langs[j*2+1]` into malloc'd `translated[j]` */
        if(tr_url) {
            url = main_tr_parse(tr_url, (char*)world.players[idx].lang, langs + j * 2, (char*)buf);
            if(!url) goto err;
            par = tr_par && *tr_par ? main_tr_parse(tr_par, (char*)world.players[idx].lang, langs + j * 2, (char*)buf) : NULL;
            f = files_ssl_open(tr_url, tr_method && *tr_method ? tr_method : "GET", par, tr_hdr, &cookie);
            if(f) {
                if(files_ssl_read(f)) translated[j] = json_get((const char*)f->buf, tr_res && *tr_res ? tr_res : "0");
                files_ssl_close(f);
            }
            if(par) free(par);
            free(url);
            if(!translated[j]) goto err;
            continue;
        }
err:    translated[j] = (char*)malloc(len + 1);
        if(translated[j]) { memcpy(translated[j], buf, len); translated[j][len] = 0; }
    }
    if(cookie) free(cookie);

    /* send out translated messages */
    for(j = 0; j < world.numplayers; j++)
        if(idx != j && !world.players[j].banned && !world.players[j].logoutd && world.players[j].remote_port &&
          memcmp(world.players[idx].lang, world.players[j].lang, 2) && map_srv_dist(&world.players[idx].data, &world.players[j].data)) {
            for(k = 0; k < dl && memcmp(langs + 2 * k, world.players[j].lang, 2); k++);
            if(k < dl && translated[k]) {
                if((l = strlen(translated[k])) > 0) {
                    if(l > 1024) l = 1024;
                    memcpy(msg + 4, translated[k], l);
                    main_udp_send(j, 12, msg, l + 4);
                }
            }
        }
    /* free resources */
    for(j = 0; j < num; j++)
        if(translated[j]) free(translated[j]);
    free(translated);
    free(langs);
}

/**
 * Bind to specified port
 */
int main_bind(char *bindip, char *port)
{
    uint8_t addr[32] = { 0 };
    int addrfam = 0, addrlen = 0, n = 1;
#ifdef __WIN32__
    DWORD nb;
    WSADATA WsaData;
    WSAStartup(MAKEWORD(2,2), &WsaData);
#endif
    memset(server_ip, 0, sizeof(server_ip));
    mbedtls_net_init( &listen_fd );
    if(!port || !*port) return 0;
    if(mbedtls_net_bind( &listen_fd, bindip, port, MBEDTLS_NET_PROTO_TCP, addr, &addrlen, &addrfam ) != 0)
        return 0;
    udpsock = socket(addrfam, SOCK_DGRAM, IPPROTO_UDP);
    if(udpsock <= 0) {
        udpsock = 0;
        mbedtls_net_free( &listen_fd );
        return 0;
    }
    if(setsockopt(udpsock, SOL_SOCKET, SO_REUSEADDR, (const char *)&n, sizeof(n)) ||
#ifdef __WIN32__
      ioctlsocket(udpsock, FIONBIO, &nb) ||
#else
      fcntl(udpsock, F_SETFL, O_NONBLOCK, 1) ||
#endif
      bind(udpsock, (const struct sockaddr*)&addr, addrlen)) {
        close(udpsock);
        mbedtls_net_free( &listen_fd );
        return 0;
    }
    inet_ntop(((struct sockaddr*)&addr)->sa_family, ((struct sockaddr*)&addr)->sa_family == AF_INET ?
        (void*)&((struct sockaddr_in*)&addr)->sin_addr : (void*)&((struct sockaddr_in6*)&addr)->sin6_addr,
        server_ip, sizeof(server_ip));
    server_port = atoi(port);
    return 1;
}

/**
 * Low level UDP sender. DO NOT CALL DIRECTLY, use main_udp_send instead
 */
int main_udp_sendto(void *ctx, const void *msg, int len)
{
    int i, idx = *((int*)ctx);
    uint8_t *data = (uint8_t*)msg;
    struct sockaddr_storage peer;

    memset(&peer, 0, sizeof(peer));
    peer.ss_family = world.players[idx].fam;
    if(peer.ss_family == AF_INET) {
        memcpy(&(((struct sockaddr_in*)&peer)->sin_addr), world.players[idx].remote_ip, 4);
        ((struct sockaddr_in*)&peer)->sin_port = world.players[idx].remote_port;
    } else {
        memcpy(&(((struct sockaddr_in6*)&peer)->sin6_addr), world.players[idx].remote_ip, 16);
        ((struct sockaddr_in6*)&peer)->sin6_port = world.players[idx].remote_port;
    }
    for(i = 0; i < len; i++)
        data[i] ^= world.players[idx].mask[i & 0xff];
    return sendto(udpsock, (const char*)data, len, 0, (struct sockaddr *)&peer, peer.ss_family == AF_INET ?
        sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6));
}

/**
 * Send an UDP message to a player
 */
int main_udp_send(int idx, int type, uint8_t *buf, int len)
{
    uint8_t data[1536];

    if(idx < 0 || idx >= world.numplayers || len < 0 || len > NETQ_MTU - 6 || !udpsock || !world.players[idx].fam) return -1;
    memset(data, 0, sizeof(data));
    *((uint16_t*)data) = (len << 5) | (type & 31);
    if(buf && len) memcpy(data + 2, buf, len);
    main_log(2, "%" FMT ": udp_send player %d packet %d len %d", pthread_self(), idx, type, len + 2);
    return netq_send(&world.players[idx].nq, data, len + 2, &idx);
}

/**
 * Receive one UDP message
 */
void main_udp_push(struct sockaddr *peer, uint8_t *buf, int len)
{
    int i, j;
    uint8_t *deny, *ip = peer->sa_family == AF_INET ?
        (uint8_t*)&((struct sockaddr_in*)peer)->sin_addr : (uint8_t*)&((struct sockaddr_in6*)peer)->sin6_addr;
    uint16_t port = peer->sa_family == AF_INET ? ((struct sockaddr_in*)peer)->sin_port : ((struct sockaddr_in6*)peer)->sin6_port;
    int l = peer->sa_family == AF_INET ? 4 : 16;
    char ipstr[INET6_ADDRSTRLEN];

    if(world.denylist) {
        for(i = 0, deny = world.denylist; i < world.numdenylist; i++, deny += 17)
            if(deny[0] == peer->sa_family && !memcmp(deny + 1, ip, l)) return;
    }
    if(verbose > 2) {
        memset(&ipstr, 0, sizeof(ipstr));
        inet_ntop(peer->sa_family, (void*)ip, ipstr, sizeof(ipstr));
        main_log(3, "%" FMT ": udp message from [%s]:%u, len %u", pthread_self(), ipstr, port, len);
    }
    /* handle UDP Hello, the only non-encrypted message */
    if(len == 16 && buf[0] == 'H' && buf[1] == 'i' && !buf[2] && !buf[3] && !buf[4] && !buf[5] && buf[6] == 0x46 && buf[7] == 1) {
        if(!buf[8] && !buf[9] && !buf[10] && !buf[11] && !buf[12] && !buf[13] && !buf[14] && !buf[15]) {
            /* sending UDP Broadcast Hello ACK */
            memset(buf, 0, 32); buf[1] = 4; buf[4] = 'T'; buf[5] = 'N'; buf[6] = 'G'; buf[7] = 'S';
            memcpy(buf + 16, (uint8_t*)&world.hdr + 16, 16);
            sendto(udpsock, (const char*)buf, 32, 0, peer, peer->sa_family == AF_INET ?
                sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6));
            return;
        }
        /* check for empty port too, because we might have received multiple instances of the same message */
        for(i = 0; i < world.numplayers; i++) {
            if(world.players[i].fam == peer->sa_family && !memcmp(world.players[i].remote_ip, ip, l) &&
              !memcmp(world.players[i].mask + 256, buf + 8, 8)) {
                if(!world.players[i].remote_port) {
                    main_log(0, "player '%s' logged in.", world.players[i].name);
                    /* the peer's ip and the OTP matches, register remote port */
                    world.players[i].remote_port = port;
                    world.players[i].logind = world.players[i].lastd = now.tv_sec;
                    world.players[i].nq.seq_out = world.players[i].nq.seq_in = world.players[i].nq.seq_ack =
                        world.players[i].nq.seq_pop = 0;
                    main_savegame();
                }
                /* sending UDP Hello ACK */
                sendto(udpsock, (const char*)buf, len, 0, peer, peer->sa_family == AF_INET ?
                    sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6));
                break;
            }
        }
        return;
    }
    /* find which player sent it */
    for(i = 0; i < world.numplayers && (world.players[i].banned || world.players[i].remote_port != port ||
        memcmp(&world.players[i].remote_ip, ip, l)); i++);
    if(i >= world.numplayers) {
        main_log(1, "%" FMT ": udp message without active client?", pthread_self());
        return;
    }
    world.players[i].lastd = now.tv_sec;
    /* this isn't a strong encryption, but it doesn't need to be. We negotiated the XOR mask through SSL,
     * and we check remote ip and port, so this is only needed against packet sniffers */
    for(j = 0; j < len; j++)
        buf[j] ^= world.players[i].mask[j & 0xff];
    main_log(2, "%" FMT ": udp_push player %d packet %d len %d", pthread_self(), i, buf[6] & 31, len - 6);
    netq_push(&world.players[i].nq, buf, len, &i);
}

/**
 * Handle one UDP message
 */
static void *main_udp_pop(void *data)
{
    int len, i = (int)(uintptr_t)data, j, k, arg1, arg2;
    uint8_t buf[1536], *ptr;

    while(netq_pend(&world.players[i].nq)) {
        len = netq_pop(&world.players[i].nq, buf, sizeof(buf));
        if(len < 2) break;
        main_log(2, "%" FMT ": udp_pop  player %d packet %d len %d", pthread_self(), i, buf[0] & 31, len);

        /*** parse UDP message ***/
        switch(buf[0] & 31) {
            /* Player Action */
            case 8:
                memcpy(&k, buf + 2, 4);
                world.players[i].data.map = SDL_SwapLE32(k);
                world.players[i].data.x = (buf[7] << 8) | buf[6];
                world.players[i].data.y = (buf[9] << 8) | buf[8];
                world.players[i].data.d = buf[10];
                world.players[i].data.altitude = buf[11];
                len -= 12;
                /* FIXME: parse remaining buffer for actions */

                /* notify other clients */
                memmove(buf + 5, buf + 2, 10);
                buf[0] = 0; k = SDL_SwapLE32(i); memcpy(buf + 1, &k, 4);
                for(j = 0; j < world.numplayers; j++)
                    if(i != j && !world.players[j].banned && !world.players[j].logoutd && world.players[j].remote_port &&
                      map_srv_neighbor(world.players[i].data.map, world.players[j].data.map))
                        main_udp_send(j, 7, buf, 15);
            break;

            /* Player Logout */
            case 9:
                world.players[i].logoutd = now.tv_sec; memset(&world.players[i].remote_ip, 0, 16);
                world.players[i].fam = 0; world.players[i].remote_port = 0;
                /* notify other clients */
                buf[0] = 1; k = SDL_SwapLE32(i); memcpy(buf + 1, &k, 4);
                for(j = 0; j < world.numplayers; j++)
                    if(i != j && !world.players[j].banned && !world.players[j].logoutd && world.players[j].remote_port &&
                      map_srv_neighbor(world.players[i].data.map, world.players[j].data.map))
                        main_udp_send(j, 7, buf, 5);
                main_savegame();
            break;

            /* Request Map */
            case 10:
                memcpy(&arg1, buf + 2, 4); arg1 = SDL_SwapLE32(arg1);
                if(arg1 >= 0 && arg1 < world.nummap) {
                    ptr = world_getmap(arg1, &len);
                    if(ptr) {
                        buf[6] = len & 0xff; buf[7] = (len >> 8) & 0xff; buf[8] = (len >> 16) & 0xff; j = 0;
                        do {
                            buf[9] = j & 0xff; buf[10] = (j >> 8) & 0xff; buf[11] = (j >> 16) & 0xff;
                            k = (len - j) > NETQ_MTU - 18 ? NETQ_MTU - 18 : len - j;
                            memcpy(buf + 12, ptr + j, k);
                            main_udp_send(i, 11, buf + 2, 10 + k);
                            j += k;
                        } while(j < len);
                        free(ptr);
                    }
                }
            break;

            /* Text Chat */
            case 12:
                /* just proxy Text Chat message if language code matches, or message starts with a " character,
                 * otherwise call the translator if enabled */
                if(len > 6) {
                    j = SDL_SwapLE32(i); memcpy(buf + 2, &j, 4);
                    for(j = k = 0; j < world.numplayers; j++)
                        if(i != j && !world.players[j].banned && !world.players[j].logoutd && world.players[j].remote_port &&
                          map_srv_dist(&world.players[i].data, &world.players[j].data)) {
                            if(buf[6] != '"' && tr_url && memcmp(world.players[i].lang, world.players[j].lang, 2)) k++;
                            else main_udp_send(j, 12, buf[6] == '"' ? buf + 7 : buf + 6, buf[6] == '"' ? len - 7 : len - 6);
                        }
                    if(k > 0) main_translate(i, k, buf + 6, len - 6);
                }
            break;

            /* Audio Chat */
            case 13:
                /* just proxy PCM data as-is. We use netq_send directly to avoid copying the buffer */
                if(len > 8) {
                    j = SDL_SwapLE32(i); memcpy(buf + 2, &j, 4);
                    for(j = 0; j < world.numplayers; j++)
                        if(i != j && !world.players[j].banned && !world.players[j].logoutd && world.players[j].remote_port &&
                          map_srv_dist(&world.players[i].data, &world.players[j].data))
                            netq_send(&world.players[j].nq, buf, len, &j);
                }
            break;

            /* Quest */
            case 14:
                memcpy(&arg1, buf + 2, 4); arg1 = SDL_SwapLE32(arg1); arg2 = buf[6];
                if(world.quests && arg1 >= 0 && arg1 < world.numqst &&
                  (!world.quests[arg1].type || world.quests[arg1].completed == -1)) {
                    /* failsafe, never record quest complete on already completed global quests */
                    if(arg2 && world.quests[arg1].completed != -1) break;
                    /* add to the player */
                    world_quest(&world.players[i].data, arg1, arg2, 0);
                    main_udp_send(i, 14, buf + 2, 5);
                    /* if this is about completing a global quest, store player in quest and cancel the quest for all the others */
                    if(world.quests[arg1].type && arg2) {
                        world.quests[arg1].completed = i; buf[6] = 2;
                        for(j = 0; j < world.numplayers; j++) {
                            if(i != j && world.players[i].data.quests) {
                                for(k = 0; k < world.players[i].data.numqst && world.players[i].data.quests[k * 2 + 1] != arg1; k++);
                                if(k < world.players[i].data.numqst) {
                                    main_udp_send(j, 14, buf + 2, 5);
                                    memcpy(&world.players[i].data.quests[k * 2], &world.players[i].data.quests[(k + 1) * 2],
                                        (world.players[i].data.numqst - k) * 2 * sizeof(int));
                                    world.players[i].data.numqst--;
                                    if(!world.players[i].data.numqst) {
                                        free(world.players[i].data.quests); world.players[i].data.quests = NULL;
                                    }
                                }
                            }
                        }
                    }
                    main_savegame();
                }
            break;

            /* Whois */
            case 15:
                memcpy(&arg1, buf + 2, 4); arg1 = SDL_SwapLE32(arg1);
                if(arg1 >= 0 && arg1 < world.numplayers && world.players[arg1].name && world.players[arg1].opts) {
                    buf[1] = 2; ptr = buf + 6; len = 5;
                    for(j = 0; j < world.numchar; j++, ptr += 4, len += 4) {
                        k = SDL_SwapLE32(world.players[arg1].opts[j]);
                        memcpy(ptr, &k, 4);
                    }
                    for(j = 0; j < 32; j++, ptr += 2, len += 2) {
                        k = SDL_SwapLE16(world.players[arg1].equip[j]);
                        memcpy(ptr, &k, 2);
                    }
                    k = strlen(world.players[arg1].name) + 1;
                    memcpy(ptr, world.players[arg1].name, k);
                    len += k;
                    main_udp_send(j, 7, buf + 1, len);
                }
            break;

            /* Inventory */
            case 16:
                memcpy(&arg1, buf + 2, 4); arg1 = SDL_SwapLE32(arg1);
                if(arg1 == -1) arg1 = i;
                if(arg1 >= 0 && arg1 < world.numplayers && world.players[arg1].data.inv && world.players[arg1].data.numinv > 0) {
                    buf[1] = 3; len = world_serializeinv(arg1, buf + 6, 1200) + 5;
                    main_udp_send(j, 7, buf + 1, len);
                }
            break;

        }
    }
    pthread_exit(NULL);
    return NULL;
}

/**
 * Handle one SSL/TLS connection
 */
static void *main_ssl_con(void *data)
{
    int ret, len, mlen = 0, i, j, k, l, ncon, x, y;
    char *o, *dirs[] = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" }, *trns[] = { "walk", "swim", "fly", "drive" };
    char *gametypes[] = { "ortho", "iso", "hexv", "hexh", "3d" };
    ssl_conn_t *conn = (ssl_conn_t *)data;
    mbedtls_net_context *client_fd = &conn->client_fd;
    mbedtls_ssl_context ssl;
    uint8_t buf[2048] /* must be as big as client.c / client_register() / msg */, tmp[16384], *s, *more = NULL;
    uint64_t curr;
    uint32_t crc;

    memset(&ssl, 0, sizeof(ssl));
    mbedtls_ssl_init( &ssl );
    mbedtls_ssl_setup( &ssl, &conf );
    mbedtls_ssl_set_bio( &ssl, client_fd, mbedtls_net_send, mbedtls_net_recv, NULL );
    while( ( ret = mbedtls_ssl_handshake( &ssl ) ) != 0 ) {
        if( ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE ) {
            main_log(1, "%" FMT ": ssl handshake error -0x%04x", pthread_self(), -ret);
            goto thread_exit;
        }
    }
    /* read query */
again:
    memset(buf, 0, sizeof(buf));
    ret = mbedtls_ssl_read( &ssl, buf, sizeof(buf) - 1 );
    if(ret == sizeof(buf) - 1) {
        while(ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
            ret = mbedtls_ssl_read( &ssl, tmp, sizeof(tmp) );
    }
    memset(tmp, 0, sizeof(tmp)); len = 0;
    if(ret <= 0) goto thread_exit;

    if((((buf[1] << 8) | buf[0]) >> 5) == ret) {
        /**** connection from a TirNanoG client ****/
        switch(buf[0] & 31) {
            /* Hello (version and assets check) */
            case 0:
                if(buf[0] != 0 || buf[1] != 1 || buf[4] != 'T' || buf[5] != 'N' || buf[6] != 'G' || buf[7] != 'P') goto err;
                main_log(2, "%" FMT ": ssl hello", pthread_self());
                tmp[1] = 8; tmp[4] = 'T'; tmp[5] = 'N'; tmp[6] = 'G'; tmp[7] = 'S';
                memcpy(tmp + 16, (uint8_t*)&world.hdr + 16, 40);
                memcpy(tmp + 56, &world.asiz, 8);
                while( ( ret = mbedtls_ssl_write( &ssl, tmp, 64 ) ) <= 0 &&
                    (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE));
                goto again;
            break;

            /* Ask for Assets Boundle */
            case 1:
                main_log(1, "%" FMT ": ssl assets request, sending %" FMT " bytes", pthread_self(), world.asiz);
                world_seektng(0, 0, 64); crc = 0;
                for(curr = 0; curr < world.asiz; curr += len) {
                    len = world.asiz - curr > sizeof(tmp) ? sizeof(tmp) : world.asiz - curr;
                    if(!len) break;
                    world_readtng(0, 0, &tmp, len);
                    crc = crc32(crc, tmp, len);
                    while( ( ret = mbedtls_ssl_write( &ssl, tmp, len ) ) <= 0 &&
                        (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE));
                    if(ret < 0) break;
                }
                memcpy(tmp, &crc, 4);
                len = 4;
            break;

            /* User Registration (Character Generation) */
            case 2:
                main_log(1, "%" FMT ": ssl registration attempt '%s'", pthread_self(), buf + 36);
                ncon = world_playercleanup(now.tv_sec);
                i = strlen((char*)buf + 36);
                if((i = world_register((char*)buf + 36, buf + 4, world.numchar > 0 ? (int32_t*)(buf + 37 + i) : NULL,
                  world.numchrattrs > 0 ? (int32_t*)(buf + 37 + i + world.numchar * sizeof(int32_t)) : NULL)) != -1) {
                    /* if there's a run once cron script, run it now to initialize new player's attributes */
                    for(j = 0, k = SCRIPT_AGAIN; j < WORLD_NUMCRON; j++)
                        if(!world.cron[j].value) {
                            k = script_load(SCRIPT_CALLER(SCRIPT_STARTUP, j), world.cron[j].bc, world.cron[j].bclen,
                                world.cron[j].tng, &world.players[i].data, NULL);
                            break;
                        }
                    main_log(0, "player '%s' registered.", world.players[i].name);
                    goto dologin;
                } else { tmp[0] = (4 << 5) | 4; tmp[2] = 0; len = 4; }
            break;

            /* User Login */
            case 3:
                main_log(1, "%" FMT ": ssl login attempt '%s'", pthread_self(), buf + 36);
                ncon = world_playercleanup(now.tv_sec);
                for(i = 0; i < world.numplayers && (world.players[i].banned || memcmp(buf + 4, &world.players[i].pass, 32) ||
                    strcmp((char*)buf + 36, world.players[i].name)); i++);
dologin:        if(i < world.numplayers && ncon < maxplayers) {
                    memcpy(&world.players[i].lang, buf + 2, 2);
                    memcpy(&world.players[i].remote_ip, &conn->peer_addr, conn->peer_fam == AF_INET ? 4 : 16);
                    world.players[i].fam = conn->peer_fam; world.players[i].remote_port = 0;
                    world.players[i].logoutd = 0;
                    /* generate random bytes for XOR mask and OTP */
                    mbedtls_ctr_drbg_random(&ctr_drbg, world.players[i].mask, sizeof(world.players[i].mask));
                    *((uint16_t*)tmp) = (270 << 5) | 5; len = 270;
                    if(!world.players[i].mask[256]) world.players[i].mask[256]++;
                    memcpy(tmp + 2, &world.players[i].mask, 264);
                    more = world_serializestate(SERTYPE_LOGIN, i, &mlen);
                    j = SDL_SwapLE32(mlen); memcpy(tmp + 266, &j, 4);
                    /* we should now receive an UDP message from the client with this OTP to register its remote port */
                } else { tmp[0] = (4 << 5) | 4; tmp[2] = ncon >= maxplayers ? 1 : 0; len = 4; }
            break;

            /* Error, Unknown Request */
            default: main_log(1, "%" FMT ": ssl unknown packet %02x", pthread_self(), buf[0] & 31); break;
        }
    } else
    if(!memcmp(buf, "GET ", 4) && admin_addrlen &&
      /* exact IP match, either IPV4 or IPv6 */
      (((conn->peer_fam ==  AF_INET ? 4 : 16) == admin_addrlen && !memcmp(conn->peer_addr, &admin_addr, admin_addrlen)) ||
      /* IPv4 tunneling over IPv6 (for example "-b :: -a 127.0.0.1") */
      (admin_addrlen == 4 && conn->peer_fam == AF_INET6 && !memcmp(conn->peer_addr, "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12) &&
      !memcmp(&conn->peer_addr[12], &admin_addr, 4))
#if 0
      /* special case, if we only allow IPv6 localhost, but got IPv4 localhost tunneled through IPv6 */
      || (admin_addrlen == 16 && conn->peer_fam == AF_INET && !memcmp(&admin_addr, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1", 16) &&
      !memcmp(&conn->peer_addr, "\0\0\0\0\0\0\0\0\0\0\xff\xff\x7f\0\0\1", 16))
#endif
      )) {
        /**** this is a REST API request, speak HTTP ****/
        for(s = buf + 4; *s && *s != ' ' && *s != '\r' && *s != '\n' && s < buf + 1023; s++);
        *s = 0;
        main_log(1, "%" FMT ": ssl admin API '%s'", pthread_self(), buf + 4);
        strcpy((char*)tmp, "HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n");
        len = strlen((char*)tmp);

        /* Get Server Status */
        if(!memcmp("/status", buf + 4, 8)) {
            j = world_playercleanup(now.tv_sec);
            len += sprintf((char*)tmp + len, "{\"now\":%" FMT ",\"uptime\":%" FMT ",\"saved\":%" FMT ",\"users\":%u,\"total\":%u,\"reg\":%s}\r\n",
                now.tv_sec, now.tv_sec - started.tv_sec, saved, j, world.numplayers, !world.noreg ? "true" : "false");
        } else

        /* Get World File Details */
        if(!memcmp("/tng", buf + 4, 5)) {
            memcpy(buf + 16, world.hdr.id, 16); buf[16 + 16] = 0;
            len += sprintf((char*)tmp + len, "{\"gameid\":\"%s\",\"rev\":%u,\"type\":\"%s\",\"tilew\":%u,\"tileh\":%u,\"mapsize\":%u,"
            "\"atlaswh\":%u,\"freq\":%u,\"fps\":%u,\"numact\":%u,\"numlyr\":%u,\"numtrn\":%u,\"maxbc\":%u,\"deltay\":%d,"
            "\"numlang\":%u,\"numtext\":%u,\"numspc\":%u,\"numfont\":%u,\"nummusic\":%u,\"numsound\":%u,\"nummovie\":%u,"
            "\"numchr\":%u,\"numcut\":%u,\"numnpc\":%u,\"numswr\":%u,\"numdlg\":%u,\"numcft\":%u,\"numqst\":%u,\"nummap\":%u,"
            "\"numobj\":%u}\r\n",
                buf+16, world.hdr.revision, world.hdr.type < 5 ? gametypes[world.hdr.type] : "???", world.hdr.tilew,
                world.hdr.tileh, 1 << world.hdr.mapsize, 1 << world.hdr.atlaswh, 44100 * (1 << world.hdr.freq), world.hdr.fps,
                world.hdr.numact, world.hdr.numlyr, world.hdr.numtrn, world.hdr.maxbc, world.hdr.deltay, world.numlang,
                world.numtext, world.numspc, world.numfont, world.nummusic, world.numsound, world.nummovie, world.numchr,
                world.numcut, world.numnpc, world.numswr, world.numdlg, world.numcft, world.numqst, world.nummap,
                world.numspr[SPR_CHR]);
        } else

        /* Synchronize to Storage (Save Game) */
        if(!memcmp("/sync", buf + 4, 6)) {
            ret = main_savegame();
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", ret ? "true" : "false");
        } else

        /* Enable / Disable Public User Registration */
        /* With public registration off, only users apriori added with the "add" command can create a character */
        if(!memcmp("/regoff", buf + 4, 8)) { world.noreg = 1; strcpy((char*)tmp + len, "{}\r\n"); len += 4; } else
        if(!memcmp("/regon", buf + 4, 7)) { world.noreg = 0; strcpy((char*)tmp + len, "{}\r\n"); len += 4; } else

        /* Get List of Registered Users */
        if(!memcmp("/users", buf + 4, 7)) {
            more = (uint8_t*)malloc(world.numplayers * (256 + 32) + 4);
            if(more) {
                mlen = sprintf((char*)more, "[");
                world_playercleanup(now.tv_sec);
                for(i = 0; i < world.numplayers; i++)
                    mlen += sprintf((char*)more + mlen, "%s\r\n{\"username\":\"%s\",\"enabled\":%s,\"online\":%s}",
                        i ? "," : "", world.players[i].name, world.players[i].banned ? "false" : "true",
                        !world.players[i].logoutd ? "true" : "false");
                strcpy((char*)more + mlen, "\r\n]\r\n"); mlen += 5;
            } else {
                strcpy((char*)tmp + len, "[]\r\n"); len += 4;
            }
        } else

        /* Add User (allow character generation for specific users in the TirNanoG client with registration off) */
        if(!memcmp("/add?u=", buf + 4, 7)) {
            urldecode((char*)buf + 11);
            ret = world.noreg ? world_register((char*)buf + 11, NULL, NULL, NULL) != -1 : 0;
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", ret ? "true" : "false");
        } else

        /* Authenticate User (also returns the language code that the user last used with the TirNanoG client */
        if(!memcmp("/auth?u=", buf + 4, 8)) {
            o = urldecode((char*)buf + 12);
            if(o[0] != 'p' || o[1] != '=') goto err; else o += 2;
            urldecode(o);
            world_hashpass(o, buf + 512);
            for(i = 0; i < world.numplayers && (world.players[i].banned || memcmp(buf + 512, &world.players[i].pass, 32) ||
                strcmp((char*)buf + 12, world.players[i].name)); i++);
            if(i < world.numplayers)
                len += sprintf((char*)tmp + len, "{\"ret\":true,\"lang\":\"%c%c\"}\r\n",
                    world.players[i].lang[0] && world.players[i].lang[1] ? world.players[i].lang[0] : 'e',
                    world.players[i].lang[0] && world.players[i].lang[1] ? world.players[i].lang[1] : 'n');
            else
                len += sprintf((char*)tmp + len, "{\"ret\":false}\r\n");
        } else

        /* Set Password */
        if(!memcmp("/pass?u=", buf + 4, 8)) {
            o = urldecode((char*)buf + 12);
            if(o[0] != 'p' || o[1] != '=') goto err; else o += 2;
            urldecode(o);
            world_hashpass(o, buf + 512);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 12, world.players[i].name); i++);
            if(i < world.numplayers)
                memcpy(world.players[i].pass, buf + 512, 32);
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers ? "true" : "false");
        } else

        /* Forcefully Logout User from the Game */
        if(!memcmp("/kick?u=", buf + 4, 8)) {
            urldecode((char*)buf + 12);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 12, world.players[i].name); i++);
            if(i < world.numplayers) {
                world.players[i].logoutd = now.tv_sec; memset(&world.players[i].remote_ip, 0, 16);
                world.players[i].fam = 0; world.players[i].remote_port = 0;
            }
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers ? "true" : "false");
        } else

        /* Ban User (kick out user and disable re-login) */
        if(!memcmp("/ban?u=", buf + 4, 7)) {
            urldecode((char*)buf + 11);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 11, world.players[i].name); i++);
            if(i < world.numplayers) {
                world.players[i].logoutd = now.tv_sec; memset(&world.players[i].remote_ip, 0, 16);
                world.players[i].fam = 0; world.players[i].remote_port = 0;
                world.players[i].banned = 1;
            }
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers ? "true" : "false");
        } else

        /* Unban User (enable login) */
        if(!memcmp("/unban?u=", buf + 4, 9)) {
            urldecode((char*)buf + 13);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 13, world.players[i].name); i++);
            if(i < world.numplayers) world.players[i].banned = 0;
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers ? "true" : "false");
        } else

        /* Get List of Denied IP Addresses */
        if(!memcmp("/denied", buf + 4, 8)) {
            more = (uint8_t*)malloc(world.numdenylist * (INET6_ADDRSTRLEN + 5) + 4);
            if(more) {
                mlen = sprintf((char*)more, "[");
                for(i = mlen = 0, s = world.denylist; i < world.numdenylist; i++, s += 17) {
                    memset(buf + 512, 0, INET6_ADDRSTRLEN + 1);
                    inet_ntop(s[0], (void*)(s + 1), (char*)buf + 512, INET6_ADDRSTRLEN);
                    if(buf[512])
                        mlen += sprintf((char*)more + mlen, "%s\r\n\"%s\"", mlen ? "," : "", buf + 512);
                }
                strcpy((char*)more + mlen, "\r\n]\r\n"); mlen += 5;
            } else {
                strcpy((char*)tmp + len, "[]\r\n"); len += 4;
            }
        } else

        /* Add to Denied List */
        if(!memcmp("/deny?u=", buf + 4, 8)) {
            urldecode((char*)buf + 12);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 12, world.players[i].name); i++);
            if(i < world.numplayers) {
                for(j = 0; j < 16 && !world.players[i].remote_ip[j]; j++);
                if(j >= 16)
                    i = world.numplayers;
                else {
                    k = world.players[i].fam == AF_INET ? 4 : 16;
                    for(j = 0, s = world.denylist; j < world.numdenylist && (s[0] != world.players[i].fam ||
                        memcmp(world.players[i].remote_ip, s + 1, k)); j++, s += 17);
                    if(j >= world.numdenylist) {
                        j = world.numdenylist++;
                        world.denylist = (uint8_t*)realloc(world.denylist, world.numdenylist * 17);
                        if(!world.denylist) { world.numdenylist = 0; i = world.numplayers; }
                        else {
                            s = world.denylist + j * 17; memset(s, 0, 17);
                            s[0] = world.players[i].fam; memcpy(s + 1, world.players[i].remote_ip, k);
                        }
                    }
                }
            }
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers ? "true" : "false");
        } else

        /* Remove from Denied List (by user name) */
        if(!memcmp("/allow?u=", buf + 4, 9)) {
            urldecode((char*)buf + 13);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 13, world.players[i].name); i++);
            if(i < world.numplayers) {
                for(j = 0; j < 16 && !world.players[i].remote_ip[j]; j++);
                if(j >= 16)
                    i = world.numplayers;
                else {
                    k = world.players[i].fam == AF_INET ? 4 : 16;
                    for(j = 0, s = world.denylist; j < world.numdenylist && (s[0] != world.players[i].fam ||
                        memcmp(world.players[i].remote_ip, s + 1, k)); j++, s += 17);
                    if(j < world.numdenylist) {
                        memcpy(world.denylist + j * 17, world.denylist + (j + 1) * 17, (world.numdenylist - j) * 17);
                        world.numdenylist--;
                        if(!world.numdenylist) { free(world.denylist); world.denylist = NULL; }
                    } else
                        i = world.numplayers;
                }
            }
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers ? "true" : "false");
        } else

        /* Remove from Denied List (by IP address) */
        if(!memcmp("/allowip?u=", buf + 4, 11)) {
            urldecode((char*)buf + 15);
            for(i = 0, s = world.denylist; i < world.numdenylist; i++, s += 17) {
                memset(buf + 512, 0, INET6_ADDRSTRLEN + 1);
                inet_ntop(s[0], (void*)(s + 1), (char*)buf + 512, INET6_ADDRSTRLEN);
                if(buf[512] && !strcmp((char*)buf + 512, (char*)buf + 15)) {
                    memcpy(world.denylist + i * 17, world.denylist + (i + 1) * 17, (world.numdenylist - i) * 17);
                    world.numdenylist--;
                    if(!world.numdenylist) { free(world.denylist); world.denylist = NULL; }
                    break;
                }
            }
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numdenylist ? "true" : "false");
        } else

        /* Get User's Status */
        if(!memcmp("/user?u=", buf + 4, 8)) {
            urldecode((char*)buf + 12);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 12, world.players[i].name); i++);
            if(i < world.numplayers) {
                map_map2tile(world.players[i].data.x, world.players[i].data.y, &x, &y);
                more = (uint8_t*)malloc(256 + (world.numchar + world.numchrattrs) * 64);
                if(more) {
                    mlen = sprintf((char*)more,
                        "{\"ret\":true,\"online\":%s,\"created\":%" FMT ",\"logind\":%" FMT ",\"logoutd\":%" FMT ",\"lang\":\"%c%c\""
                        ",\"map\":\"%s\",\"x\":%u.%u,\"y\":%u.%u,\"dir\":\"%s\",\"transport\":\"%s\",\"banned\":%s",
                        !world.players[i].logoutd ? "true" : "false", world.players[i].created, world.players[i].logind,
                        world.players[i].logoutd,
                        world.players[i].lang[0] && world.players[i].lang[1] ? world.players[i].lang[0] : 'e',
                        world.players[i].lang[0] && world.players[i].lang[1] ? world.players[i].lang[1] : 'n',
                        world.map && world.players[i].data.map >= 0 && world.players[i].data.map < world.nummap ?
                            world.map[world.players[i].data.map].name : "???",
                        x, world.players[i].data.x % world.tilew, y, world.players[i].data.y % world.tileh,
                        world.players[i].data.d < 8 ? dirs[(int)world.players[i].data.d] : "???",
                        world.players[i].data.transport < 4 ? trns[(int)world.players[i].data.transport] : "???",
                        world.players[i].banned ? "true" : "false");
                    for(j = k = 0; j < 16 && !world.players[i].remote_ip[j]; j++);
                    if(j < 16) {
                        k = world.players[i].fam == AF_INET ? 4 : 16;
                        for(j = 0, s = world.denylist; j < world.numdenylist && (s[0] != world.players[i].fam ||
                            memcmp(world.players[i].remote_ip, s + 1, k)); j++, s += 17);
                        k = (j < world.numdenylist);
                    }
                    mlen += sprintf((char*)more + mlen, ",\"denied\":%s,\"charopts\":{", k ? "true" : "false");
                    if(world.chars && world.players[i].opts)
                        for(j = 0; j < world.numchar; j++)
                            mlen += sprintf((char*)more + mlen, "%s\r\n\"%s\":%u.%u", j ? "," : "", world.chars[j].name,
                                world.players[i].opts[j] >> 8, world.players[i].opts[j] & 0xff);
                    mlen += sprintf((char*)more + mlen, "\r\n},\"attributes\":{");
                    if(world.attrs && world.players[i].data.attrs)
                        for(j = k = 0; j < world.numattrs; j++)
                            if(world.attrs[j].type < ATTR_GLOBVAR) {
                                mlen += sprintf((char*)more + mlen, "%s\r\n\"%s\":%d", k ? "," : "", world.attrs[j].name,
                                    world.players[i].data.attrs[k]);
                                k++;
                            }
                    mlen += sprintf((char*)more + mlen, "\r\n}}\r\n");
                } else
                    /* memory allocation failed, only report basic properties which surely fit into tmp buffer */
                    len += sprintf((char*)tmp + len,
                        "{\"ret\":true,\"online\":%s,\"created\":%" FMT ",\"logind\":%" FMT ",\"logoutd\":%" FMT ",\"lang\":\"%c%c\""
                        ",\"map\":\"%s\",\"x\":%u.%u,\"y\":%u.%u,\"dir\":\"%s\",\"transport\":\"%s\",\"banned\":%s}\r\n",
                        !world.players[i].logoutd ? "true" : "false", world.players[i].created, world.players[i].logind,
                        world.players[i].logoutd,
                        world.players[i].lang[0] && world.players[i].lang[1] ? world.players[i].lang[0] : 'e',
                        world.players[i].lang[0] && world.players[i].lang[1] ? world.players[i].lang[1] : 'n',
                        world.map && world.players[i].data.map >= 0 && world.players[i].data.map < world.nummap ?
                            world.map[world.players[i].data.map].name : "???",
                        x, world.players[i].data.x % world.tilew, y, world.players[i].data.y % world.tileh,
                        world.players[i].data.d < 8 ? dirs[world.players[i].data.d] : "???",
                        world.players[i].data.transport < 4 ? trns[(int)world.players[i].data.transport] : "???",
                        world.players[i].banned ? "true" : "false");
            } else
                len += sprintf((char*)tmp + len, "{\"ret\":false}\r\n");
        } else

        /* Set User's Attribute */
        if(!memcmp("/set?u=", buf + 4, 7)) {
            o = urldecode((char*)buf + 11); j = world.numattrs;
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 11, world.players[i].name); i++);
            if(i < world.numplayers && world.attrs && world.players[i].data.attrs) {
                for(j = k = 0; j < world.numattrs; j++)
                    if(world.attrs[j].type < ATTR_GLOBVAR) {
                        l = strlen(world.attrs[j].name);
                        if(!memcmp(o, world.attrs[j].name, l) && o[l] == '=') {
                            o += l + 1; world.players[i].data.attrs[k] = atoi(o);
                            if(world.players[i].data.attrs[k] < -WORLD_ATTRMAX) world.players[i].data.attrs[k] = -WORLD_ATTRMAX;
                            if(world.players[i].data.attrs[k] > WORLD_ATTRMAX) world.players[i].data.attrs[k] = WORLD_ATTRMAX;
                            break;
                        }
                        k++;
                    }
            }
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", j < world.numattrs ? "true" : "false");
        } else

        /* Get User's Inventory */
        if(!memcmp("/inv?u=", buf + 4, 7)) {
            urldecode((char*)buf + 11);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 11, world.players[i].name); i++);
            if(i < world.numplayers && world.players[i].data.inv &&
              (more = (uint8_t*)malloc(16 + world.players[i].data.numinv * 64))) {
                mlen = sprintf((char*)more, "[");
                for(j = 0; j < world.players[i].data.numinv; j++)
                    mlen += sprintf((char*)more + mlen, "%s\r\n{\"qty\":%d,\"obj\":%d}", j ? "," : "",
                        world.players[i].data.inv[j].qty, world.players[i].data.inv[j].obj.idx);
                mlen += sprintf((char*)more + mlen, "\r\n]\r\n");
            } else
                len += sprintf((char*)tmp + len, "[]\r\n");
        } else

        /* Get User's Skills */
        if(!memcmp("/skills?u=", buf + 4, 10)) {
            urldecode((char*)buf + 14);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 14, world.players[i].name); i++);
            if(i < world.numplayers && world.players[i].data.skills &&
              (more = (uint8_t*)malloc(16 + world.players[i].data.numskl * 32))) {
                mlen = sprintf((char*)more, "[");
                for(j = 0; j < world.players[i].data.numskl; j++)
                    mlen += sprintf((char*)more + mlen, "%s\r\n{\"level\":%d,\"obj\":%d}", j ? "," : "",
                        world.players[i].data.skills[j].qty, world.players[i].data.skills[j].obj.idx);
                mlen += sprintf((char*)more + mlen, "\r\n]\r\n");
            } else
                len += sprintf((char*)tmp + len, "[]\r\n");
        } else

        /* Get User's Quests */
        if(!memcmp("/quests?u=", buf + 4, 10)) {
            urldecode((char*)buf + 14);
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 14, world.players[i].name); i++);
            if(i < world.numplayers && world.players[i].data.quests &&
              (more = (uint8_t*)malloc(16 + world.players[i].data.numqst * 128))) {
                mlen = sprintf((char*)more, "[");
                for(j = 0; j < world.players[i].data.numqst; j++)
                    mlen += sprintf((char*)more + mlen, "%s\r\n{\"completed\":%s,\"qst\":\"%s\"}", j ? "," : "",
                        world.players[i].data.quests[j * 2] ? "true" : "false",
                        world.quests[world.players[i].data.quests[j * 2 + 1]].name);
                mlen += sprintf((char*)more + mlen, "\r\n]\r\n");
            } else
                len += sprintf((char*)tmp + len, "[]\r\n");
        } else

        /* Add Item to User */
        if(!memcmp("/give?u=", buf + 4, 8)) {
            o = urldecode((char*)buf + 12);
            if(o[0] != 'q' || o[1] != '=') goto err; else o += 2;
            k = atoi(o); while((uintptr_t)o < (uintptr_t)buf + 512 && *o && o[-1] != '&') o++;
            if(o[0] != 'o' || o[1] != '=') goto err; else o += 2;
            l = atoi(o); if(k < 0 || l < 0 || l >= world.numspr[SPR_CHR]) goto err;
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 12, world.players[i].name); i++);
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers &&
                world_item(i, k, l) ? "true" : "false");
        } else

        /* Take Item from User */
        if(!memcmp("/take?u=", buf + 4, 8)) {
            o = urldecode((char*)buf + 12);
            if(o[0] != 'q' || o[1] != '=') goto err; else o += 2;
            k = atoi(o); while((uintptr_t)o < (uintptr_t)buf + 512 && *o && o[-1] != '&') o++;
            if(o[0] != 'o' || o[1] != '=') goto err; else o += 2;
            l = atoi(o); if(k < 0 || l < 0 || l >= world.numspr[SPR_CHR]) goto err;
            for(i = 0; i < world.numplayers && strcmp((char*)buf + 12, world.players[i].name); i++);
            client_eqchange = 0;
            len += sprintf((char*)tmp + len, "{\"ret\":%s}\r\n", i < world.numplayers &&
                world_item(i, -k, l) ? "true" : "false");
            if(client_eqchange) {
                client_eqchange = 0;
                k = SDL_SwapLE32(i); memcpy(buf + 1, &k, 4);
                buf[0] = 4; memcpy(buf + 5, world.players[i].equip, 64); memcpy(buf + 69, world.players[i].belt, 64);
                for(j = 0; j < world.numplayers; j++)
                    if(i != j && !world.players[j].banned && !world.players[j].logoutd && world.players[j].remote_port &&
                      map_srv_neighbor(world.players[i].data.map, world.players[j].data.map))
                        main_udp_send(j, 7, buf, 133);
            }
        } else

            goto err;
    } else {
        /* send error code */
err:    main_log(1, "%" FMT ": ssl sending 404", pthread_self());
        strcpy((char*)tmp, "HTTP/1.0 404 Not found\r\n\r\n");
        len = strlen((char*)tmp);
        if(more) { free(more); more = NULL; }
    }

    /* send standard message with HTTP header */
    if(len)
        while( ( ret = mbedtls_ssl_write( &ssl, tmp, len ) ) <= 0 &&
            (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE));

    /* send any additional, dynamically allocated output */
    if(more) {
        while( ( ret = mbedtls_ssl_write( &ssl, more, mlen ) ) <= 0 &&
            (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE));
        free(more);
    }
    while( ( ret = mbedtls_ssl_close_notify( &ssl ) ) < 0 &&
        (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE));

thread_exit:
    main_log(1, "%" FMT ": tcp connection closed", pthread_self());
    mbedtls_net_free( client_fd );
    mbedtls_ssl_free( &ssl );
    free(conn);
    pthread_exit(NULL);
    return NULL;
}

/**
 * The SSL/TLS server
 */
static void *main_ssl_srv(void *data)
{
    mbedtls_net_context client_fd;
    int ret, i, t;
    uint8_t *deny, addr[16];
    ssl_conn_t *conn;
    size_t addrlen;
    char ipstr[INET6_ADDRSTRLEN];
    pthread_t ssl_con;

    (void)data;
    while(1) {
        mbedtls_net_init( &client_fd );
        if( ( ret = mbedtls_net_accept( &listen_fd, &client_fd, &addr, sizeof(addr), &addrlen ) ) != 0 ) {
            main_log(0, "tcp accept returned %d", ret);
            continue;
        }
        t = addrlen == 4 ? AF_INET : AF_INET6;
        if(!inet_ntop(t, addr, ipstr, sizeof(ipstr))) {
            mbedtls_net_free( &client_fd );
            main_log(1, "tcp connection from unknown IP, skipping");
            continue;
        }
        if(world.denylist) {
            for(i = 0, deny = world.denylist; i < world.numdenylist; i++, deny += 17)
                if(deny[0] == t && !memcmp(deny + 1, addr, addrlen)) break;
            if(i < world.numdenylist) {
                mbedtls_net_free( &client_fd );
                main_log(1, "tcp connection from denied IP %s, skipping", ipstr);
                continue;
            }
        }
        conn = malloc(sizeof(ssl_conn_t));
        if(!conn) {
            mbedtls_net_free( &client_fd );
            main_log(0, "memory allocation error");
            continue;
        }
        memcpy(&conn->client_fd, &client_fd, sizeof(client_fd));
        memcpy(&conn->peer_addr, &addr, sizeof(addr));
        conn->peer_fam = t;
        ssl_con = 0;
        if(pthread_create(&ssl_con, NULL, main_ssl_con, conn)) {
            mbedtls_net_free( &conn->client_fd );
            free(conn);
            main_log(0, "unable to start tcp client thread");
        } else
            main_log(2, "%" FMT ": tcp connection from %s", ssl_con, ipstr);
    }
    return NULL;
}

/**
 * Stop server, sigint handler
 */
static void main_stop(int signal)
{
    (void)signal;
    if(ssl_thread) {
        pthread_cancel(ssl_thread);
        pthread_join(ssl_thread, NULL);
    }
    main_freetranslator();
    mbedtls_x509_crt_free( &srvcert );
    mbedtls_pk_free( &pkey );
    mbedtls_ctr_drbg_free( &ctr_drbg );
    mbedtls_entropy_free( &entropy );
    mbedtls_ssl_config_free( &conf );
    mbedtls_net_free( &listen_fd );
    if(udpsock) close(udpsock);
    main_savegame();
    script_free();
    world_free();
    main_log(0, "----- tngs gracefully stopped -----");
    if(logf) fclose(logf);
#ifdef __WIN32__
    WSACleanup();
#endif
    if(tui) {
        printf("\r\nPress Enter to close the window..."); fflush(stdout);
        getchar();
    }
    exit(0);
}

/**
 * Start server
 */
void main_start()
{
    int ret, i;
    socklen_t addrlen;
    uint8_t buf[1536];
    struct sockaddr_storage peer;
    struct timespec tv;
    uint64_t last;
    pthread_t udp_msg;

    if(foreground) {
        main_hdr();
        printf("  You should have received a copy of the GNU General Public License\r\n"
            "  along with this program; if not, write to the Free Software\r\n"
            "  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.\r\n\r\n");
        fflush(stdout);
    }

    signal(SIGINT, main_stop);
    signal(SIGTERM, main_stop);
    gettimeofday(&started, NULL);
    now = started; last = saved = now.tv_sec; main_tick = 0;
    script_init();
    main_log(0, "----- tngs started (build %u) -----", BUILD);
    main_log(0, "listening on [%s] port %d, maxplayers %d", server_ip, server_port, maxplayers);
    main_log(0, "world %s", main_fn);
    main_log(0, "state %s", savefile ? savefile : "(none)");
    mbedtls_x509_crt_init( &srvcert );
    mbedtls_pk_init( &pkey );
    mbedtls_entropy_init( &entropy );
    mbedtls_ctr_drbg_init( &ctr_drbg );
    mbedtls_ssl_config_init( &conf );
    /* we are using only hardwired data, so we assume none of these can fail */
    mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)"tngs", 4);
    mbedtls_x509_crt_parse_der( &srvcert, binary_crt_der, sizeof(binary_crt_der) );
    mbedtls_pk_parse_key( &pkey, binary_key_der, sizeof(binary_key_der), NULL, 0 );
    mbedtls_ssl_config_defaults( &conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT );
    mbedtls_ssl_conf_rng( &conf, mbedtls_ctr_drbg_random, &ctr_drbg );
    mbedtls_ssl_conf_own_cert( &conf, &srvcert, &pkey );
    if(pthread_create(&ssl_thread, NULL, main_ssl_srv, NULL))
        main_log(0, "unable to start ssl/tls tcp server thread");
    /* do the main game cycle */
    while(1) {
        main_gettick();
        /* push new messages */
        memset(&peer, 0, sizeof(peer));
        addrlen = sizeof(peer);
        ret = recvfrom(udpsock, (char*)buf, sizeof(buf), 0, (struct sockaddr *)&peer, &addrlen);
        if(ret > 0)
            main_udp_push((struct sockaddr *)&peer, buf, ret);
        /* pop messages */
        for(i = 0; i < world.numplayers; i++)
            if(world.players[i].remote_port && !world.players[i].banned && !world.players[i].logoutd &&
              netq_pend(&world.players[i].nq)) {
                udp_msg = 0;
                if(pthread_create(&udp_msg, NULL, main_udp_pop, (void*)(uintptr_t)i))
                    main_log(0, "unable to start udp client thread");
            }
        /* do housekeeping every whole second */
        if(last != (uint64_t)now.tv_sec) {
            last = now.tv_sec;
            world_playercleanup(now.tv_sec);
            /* send ping to clients not responding for 5 secs */
            for(i = 0; i < world.numplayers; i++)
                if(!world.players[i].logoutd && world.players[i].lastd + 5 < (uint64_t)now.tv_sec)
                    main_udp_send(i, 7, NULL, 0);
            /* autosave */
            if(saveival > 0 && savefile && saved + saveival <= (uint64_t)now.tv_sec)
                if(world_savestate(savefile, NULL, 0, 0)) {
                    main_log(1, "autosaving world state");
                    saved = now.tv_sec;
                }
        }
        /* execute game logic */
        script_crons();
        /* sleep 1/1000th second to ease the server machine from huge cpu load */
        tv.tv_sec = 0; tv.tv_nsec = 1000000;
        nanosleep(&tv, NULL);
    }
}

/**
 * Parse command line
 */
int main_cli(int argc, char **argv)
{
    int i, j;

    /* parse command line */
    for(i = 1; i < argc; i++) {
        if(!memcmp(argv[i], "--help", 6) || !strcmp(argv[i], CLIFLAG "h") || !strcmp(argv[i], CLIFLAG "?")) goto usage;
        if(argv[i][0] == CLIFLAG[0]) {
            switch(argv[i][1]) {
                case 'f': foreground = 1; break;
                case 'p': port = argv[++i]; break;
                case 'b': bindip = argv[++i]; break;
                case 'a': adminip = argv[++i]; break;
                case 'm': maxplayers = atoi(argv[++i]); break;
                case 'l': logfile = argv[++i]; break;
                case 's': savefile = argv[++i]; break;
                case 'S': saveival = atoi(argv[++i]); break;
                case 't': main_gettranslator(argv[++i]); break;
                case 'r': noreg = 1; break;
                case 'v': for(j = 1; argv[i][j] == 'v'; j++) verbose++; break;
                default:
usage:              main_hdr();
                    printf("  tngs "
#ifndef __WIN32__
                        "[" CLIFLAG "f] "
#endif
                        "[" CLIFLAG "p <port>] [" CLIFLAG "b <ip>] [" CLIFLAG "a <ip>] [" CLIFLAG "m <num>] "
                        "[" CLIFLAG "l <log>] [" CLIFLAG "s <save>] [" CLIFLAG "S <sec>] [" CLIFLAG "t <config>] "
                        "[" CLIFLAG "r] [" CLIFLAG "v|" CLIFLAG "vv] <game file>\r\n\r\n");
#ifndef __WIN32__
                    printf("  " CLIFLAG "f            run in foreground, do not daemonize\r\n");
#endif
                    printf("  " CLIFLAG "p <port>     port to listen on (default 4433)\r\n");
                    printf("  " CLIFLAG "b <ip>       bind server to this IP\r\n");
                    printf("  " CLIFLAG "a <ip>       allow admin connection from this IP\r\n");
                    printf("  " CLIFLAG "m <num>      number of maximum player connections\r\n");
                    printf("  " CLIFLAG "l <log>      redirect log to this file\r\n");
                    printf("  " CLIFLAG "s <save>     save game state to this file\r\n");
                    printf("  " CLIFLAG "S <sec>      autosave game every N seconds (default 3600)\r\n");
                    printf("  " CLIFLAG "t <config>   use translator config file (in JSON format)\r\n");
                    printf("  " CLIFLAG "r            disable new player registration\r\n");
                    printf("  " CLIFLAG "v|" CLIFLAG "vv        set verbosity level\r\n");
                    return 0;
                break;
            }
        } else
            main_fn = argv[i];
    }
    if(!main_fn || !port || atoi(port) < 1 || atoi(port) > 65535 || maxplayers < 2) goto usage;
    return main_run();
}
#ifndef __WIN32__
int main(int argc, char **argv)
{
    int i;
    for(i = 1; i < argc && memcmp(argv[i], "--tui", 5); i++);
    if(i < argc) return main_tui();
    if(!isatty(0) && argc < 2) return main_runtui(argv[0]);
    return main_cli(argc, argv);
}
#endif

/**
 * The real main procedure (sort of)
 */
int main_run()
{
    FILE *f = NULL;
    char *keyfile = NULL, *s, lickey[16];
#ifndef __WIN32__
    pid_t pid;
#endif

    /* check the game file */
    memset(&world, 0, sizeof(world));
    if(!memcmp(main_fn, "file://", 7)) main_fn += 7;
    if(memcmp(main_fn, "tng://", 6))
        f = files_open(main_fn, "rb");
    if(!f) {
        printf("tngs: unable to open game file '%s'\r\n", main_fn);
        return 1;
    }
    if(!fread(&world.hdr, 1, sizeof(world.hdr), f) || memcmp(&world.hdr.magic, TNG_MAGIC, 16)) {
        fclose(f);
        printf("tngs: not a valid game file '%s'\r\n", main_fn);
        return 1;
    }
    fclose(f);
    if(!tng_compat(&world.hdr)) {
        printf("tngs: you need the latest server software to play '%s'\r\n", main_fn);
        return 1;
    }
    /* load the license key if needed */
    memset(lickey, 0, sizeof(lickey));
    if(world.hdr.enc) {
        keyfile = (char*)malloc(strlen(main_fn) + 16);
        if(!keyfile) {
            printf("tngs: memory allocation error\r\n");
            return 1;
        }
        strcpy(keyfile, main_fn);
        s = strrchr(keyfile, SEP[0]);
        if(!s) s = keyfile; else s++;
        strcpy(s, "key.txt");
        f = files_open(keyfile, "rb");
        if(!f || fread(lickey, 1, 14, f) != 14) {
            printf("tngs: unable to read license key '%s'\r\n", keyfile);
            if(f) fclose(f);
            free(keyfile);
            return 1;
        }
        fclose(f);
        free(keyfile);
        if(!lickey[13] || lickey[4] != '-' || lickey[9] != '-' || !tng_lickey(lickey, world.hdr.enc)) {
            printf("tngs: invalid license key '%s'\r\n", lickey);
            return 1;
        }
    }
    /* get the admin peer's address */
    memset(&admin_addr, 0, sizeof(admin_addr)); admin_addrlen = 0;
    if(adminip) {
        if(inet_pton(AF_INET, adminip, &admin_addr) == 1) admin_addrlen = 4; else
        if(inet_pton(AF_INET6, adminip, &admin_addr) == 1) admin_addrlen = 16; else {
            printf("tngs: unable to interpret admin address '%s'\r\n", adminip);
            return 1;
        }
    }
    /* open network ports */
    if(!main_bind(bindip, port)) {
        printf("tngs: unable to bind to 'tng://[%s]:%s'\r\n", bindip ? bindip : "*", port);
        return 1;
    }
    /* open the logfile */
    if(logfile) {
        logf = files_open(logfile, "a+");
        if(!logf) {
            mbedtls_net_free( &listen_fd );
            close(udpsock);
            printf("tngs: unable to open logfile '%s'\r\n", logfile);
            return 1;
        }
    }
    /* load the world */
    if(!world_loadtng(main_fn)) {
        if(logf) fclose(logf);
        mbedtls_net_free( &listen_fd );
        close(udpsock);
        printf("tngs: unable to load the world from the game file '%s'\r\n", main_fn);
        return 1;
    }
    world_init(NULL);
    world_loadneightbors();
    world.noreg = noreg;
    map_init();
    if(savefile && !world_loadstate(savefile)) {
        if(!tui) printf("tngs: unable to load the world state from '%s'\r\n", savefile);
        /* no return, this isn't a fatal error */
    }
    /* start the server */
#ifndef __WIN32__
    if(!foreground) {
        pid = fork();
        if(pid < 0) {
            if(logf) fclose(logf);
            mbedtls_net_free( &listen_fd );
            close(udpsock);
            printf("tngs: unable to fork\n");
            return 1;
        }
        if(!pid) {
            signal(SIGCHLD, SIG_IGN);
            signal(SIGHUP, SIG_IGN);
            if(setsid() < 0) {
                if(logf) fclose(logf);
                mbedtls_net_free( &listen_fd );
                close(udpsock);
                printf("tngs: setsid failed\n");
                return 1;
            }
            pid = fork();
            if(pid < 0) {
                if(logf) fclose(logf);
                mbedtls_net_free( &listen_fd );
                close(udpsock);
                printf("tngs: unable to fork\n");
                return 1;
            }
            if(!pid) {
                close(0);
                close(1);
                close(2);
                stdin = fopen("/dev/null", "r");
                stdout = fopen("/dev/null", "w+");
                stderr = fopen("/dev/null", "w+");
                main_start();
            }
            printf("tngs: daemon started pid %u\n", pid);
        }
    } else
#endif
        main_start();
    return 0;
}
