diff --git a/LINUX_BUILD.md b/LINUX_BUILD.md new file mode 100644 index 0000000000..7d73d392ef --- /dev/null +++ b/LINUX_BUILD.md @@ -0,0 +1,139 @@ +# Meridian 59 Server - Linux Build Instructions + +## Prerequisites (Ubuntu/Debian) + +```bash +sudo dpkg --add-architecture i386 +sudo apt-get update +sudo apt-get install make gcc-multilib g++-multilib flex bison \ + libjansson-dev libjansson-dev:i386 \ + libpq-dev libpq-dev:i386 \ + python3 +``` + +## Build + +Build everything (server, compiler, KOD, resources, utilities): + +```bash +make -f makefile.linux +``` + +This builds: +- `blakserv` - the game server (copied to `run/server/`) +- `bc` - the KOD compiler (copied to `bin/`) +- `rscmerge` - resource merge tool (copied to `bin/`) +- All `.bof`, `.rsc`, `.rsb` files (copied to `run/server/rsc/` and `run/server/memmap/`) +- Room files (copied to `run/server/rooms/`) + +To clean all build artifacts: + +```bash +make -f makefile.linux clean +``` + +## Configuration + +```bash +cd run/server +cp blakserv.cfg-linux blakserv.cfg +``` + +Edit `blakserv.cfg` as needed. Key settings: + +```ini +[Socket] +Port 5959 +MaintenancePort 9998 +MaintenanceMask ::ffff:127.0.0.1;::1 + +[Resource] +Language 1 + +[MySQL] +Enabled No +``` + +Set `[MySQL] Enabled Yes` if using a database (see Database section below). + +## Run + +```bash +cd run/server +./blakserv # foreground (Ctrl+C to stop) +./blakserv & # background +``` + +Default ports: **5959** (game), **9998** (admin maintenance) + +## Admin Commands + +Via the admin script: + +```bash +./blakadmin.sh show status +./blakadmin.sh show accounts +./blakadmin.sh who +./blakadmin.sh save game +./blakadmin.sh garbage +./blakadmin.sh "send o 0 updatedatabase" +./blakadmin.sh create account admin username password email +./blakadmin.sh shutdown +``` + +Interactive mode: + +```bash +./blakadmin.sh +blakadm> show status +blakadm> bye +``` + +Or via telnet/netcat directly: + +```bash +telnet 127.0.0.1 9998 +echo "show status" | nc 127.0.0.1 9998 +``` + +## Logs + +```bash +tail -f run/server/channel/*.txt +``` + +Files: `debug.txt`, `error.txt`, `log.txt`, `god.txt`, `admin.txt` + +## Database (Optional) + +The server can optionally log game statistics (player logins, deaths, damage, money, +wiki data, etc.) to a PostgreSQL database. This is not required for the server to run. + +To enable, install PostgreSQL and configure: + +```ini +[MySQL] +Enabled Yes +Host 127.0.0.1 +Port 5432 +Username blakserv +Password your_password +Database meridian59 +``` + +Note: The config section is named `[MySQL]` for compatibility with the Windows version, +but on Linux it connects to PostgreSQL via libpq. + +The server creates all required tables automatically on first connect. + +## Architecture + +Single-threaded epoll main loop with a separate PostgreSQL writer thread +for async database operations. + +Key Linux-specific files: +- `blakserv/osd_linux.c/h` - OS-dependent types and stubs +- `blakserv/osd_epoll.c` - Main loop (epoll socket multiplexing + timer wakeup via eventfd) +- `blakserv/database_pg.c/h` - PostgreSQL database layer (replaces MySQL) +- `blakserv/main.c` - Unified Windows/Linux main with `#ifdef` +- `util/rscmerge.c` - Resource merge with deterministic file sorting diff --git a/blakcomp/blakcomp.h b/blakcomp/blakcomp.h index d1368d9639..8d101e62f1 100644 --- a/blakcomp/blakcomp.h +++ b/blakcomp/blakcomp.h @@ -17,8 +17,12 @@ #ifdef BLAK_PLATFORM_LINUX #include +#include +#include #define stricmp strcasecmp #define O_BINARY 0 +#define _MAX_PATH PATH_MAX +#define _mkdir(d) mkdir((d), 0755) #endif #include diff --git a/blakcomp/blakcomp.l b/blakcomp/blakcomp.l index 021d3f0da6..fa8bda59f5 100644 --- a/blakcomp/blakcomp.l +++ b/blakcomp/blakcomp.l @@ -606,8 +606,13 @@ void add_include_directory(char *dirname) */ void check_output_directory(char *dirname) { +#ifdef BLAK_PLATFORM_LINUX + sprintf(bof_output_dir, "%s/loadkod", dirname); + sprintf(rsc_output_dir, "%s/rsc", dirname); +#else sprintf(bof_output_dir, "%s\\loadkod", dirname); sprintf(rsc_output_dir, "%s\\rsc", dirname); +#endif if (access(dirname, 4) != 0) { @@ -780,12 +785,14 @@ int main(int argc, char **argv) } } -#ifdef BLAK_PLATFORM_WINDOWS if (directory_mode) compile_directory_mode(); else -#endif +#ifdef BLAK_PLATFORM_LINUX + compile_file_list("./", file_list); +#else compile_file_list(".\\", file_list); +#endif return !generate_code; } diff --git a/blakcomp/codegen.c b/blakcomp/codegen.c index a1c8b545a9..c51885d450 100644 --- a/blakcomp/codegen.c +++ b/blakcomp/codegen.c @@ -197,7 +197,12 @@ void codegen_filename(char *filename) if (directory_mode) { - char *fname = strrchr(filename, '\\') + 1; +#ifdef BLAK_PLATFORM_LINUX + char *fname = strrchr(filename, '/'); +#else + char *fname = strrchr(filename, '\\'); +#endif + fname = fname ? fname + 1 : filename; len = strlen(fname); memcpy(&(codegen_buffer[codegen_buffer_position]), fname, len); codegen_buffer_position += len; @@ -696,7 +701,7 @@ int codegen_switch(switch_stmt_type s, int numlocals) /* Backpatch continue statements in loop body */ for (p = current_loop->for_continue_list; p != NULL; p = p->next) - BackpatchGotoUnconditional(outfile, (int)p->data, FileCurPos(outfile)); + BackpatchGotoUnconditional(outfile, (intptr_t)p->data, FileCurPos(outfile)); /* Go back and fill in destination address for unconditional goto */ if (!default_stmt) @@ -739,12 +744,12 @@ void codegen_exit_loop(void) /* Backpatch break statements to jump to end of loop */ for (p = current_loop->break_list; p != NULL; p = p->next) - BackpatchGotoUnconditional(outfile, (int) p->data, FileCurPos(outfile)); + BackpatchGotoUnconditional(outfile, (intptr_t) p->data, FileCurPos(outfile)); current_loop->break_list = list_delete(current_loop->break_list); /* Backpatch conditional goto statements to jump to end of loop */ for (p = current_loop->conditional_goto_list; p != NULL; p = p->next) - BackpatchGotoConditional(outfile, (int)p->data, FileCurPos(outfile)); + BackpatchGotoConditional(outfile, (intptr_t)p->data, FileCurPos(outfile)); current_loop->conditional_goto_list = list_delete(current_loop->conditional_goto_list); /* Remove current list from loop "stack" */ @@ -818,7 +823,7 @@ int codegen_dowhile(while_stmt_type s, int numlocals) /* Backpatch continue statements in loop body */ for (p = current_loop->for_continue_list; p != NULL; p = p->next) - BackpatchGotoUnconditional(outfile, (int)p->data, FileCurPos(outfile)); + BackpatchGotoUnconditional(outfile, (intptr_t)p->data, FileCurPos(outfile)); numtemps = codegen_conditional_goto(s->condition, numlocals, &sourceval); if (numtemps > our_maxlocal) @@ -891,7 +896,7 @@ int codegen_for(for_stmt_type s, int numlocals) /* Backpatch continue statements in loop body */ for (p = current_loop->for_continue_list; p != NULL; p = p->next) - BackpatchGotoUnconditional(outfile, (int)p->data, FileCurPos(outfile)); + BackpatchGotoUnconditional(outfile, (intptr_t)p->data, FileCurPos(outfile)); /* Step 4: Execute statements from assign list (iterators) */ /* If no iteration, list will be NULL. */ @@ -985,7 +990,7 @@ int codegen_foreach(foreach_stmt_type s, int numlocals) /* Backpatch continue statements in loop body */ for (p = current_loop->for_continue_list; p != NULL; p = p->next) - BackpatchGotoUnconditional(outfile, (int)p->data, FileCurPos(outfile)); + BackpatchGotoUnconditional(outfile, (intptr_t)p->data, FileCurPos(outfile)); /**** Statement #4: temp = Rest(temp) ****/ /* Can reuse temp_expr from statement #3 above */ @@ -1429,8 +1434,13 @@ void codegen(char *kod_fname, char *bof_fname) write_resources(temp); // Copy bof to output dir (like old instbofrsc.bat). +#ifdef BLAK_PLATFORM_LINUX + dircompile_copy_files(bof_fname, temp, strrchr(bof_fname, '/'), + strrchr(temp, '/')); +#else dircompile_copy_files(bof_fname, temp, strrchr(bof_fname, '\\'), strrchr(temp, '\\')); +#endif } if (!directory_mode) diff --git a/blakcomp/dircompile.c b/blakcomp/dircompile.c index 55272b4020..b970fbc652 100644 --- a/blakcomp/dircompile.c +++ b/blakcomp/dircompile.c @@ -10,15 +10,38 @@ */ #include "blakcomp.h" + +#ifdef BLAK_PLATFORM_WINDOWS #include "Windows.h" #include "psapi.h" - -#if defined(WIN32) || defined(WIN64) // Copied from linux libc sys/stat.h: #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) #endif +#ifdef BLAK_PLATFORM_LINUX +#include +#include +#define _fullpath(abs, rel, maxlen) realpath((rel), (abs)) +#define DIRSEP '/' +#define DIRSEPSTR "/" +#define CopyFile(src, dst, fail_if_exists) \ + do { \ + FILE *_in = fopen((src), "rb"); \ + FILE *_out = fopen((dst), "wb"); \ + if (_in && _out) { \ + char _buf[4096]; size_t _n; \ + while ((_n = fread(_buf, 1, sizeof(_buf), _in)) > 0) \ + fwrite(_buf, 1, _n, _out); \ + } \ + if (_in) fclose(_in); \ + if (_out) fclose(_out); \ + } while(0) +#else +#define DIRSEP '\\' +#define DIRSEPSTR "\\" +#endif + extern list_type directory_list; extern list_type file_list; extern int codegen_ok; @@ -74,7 +97,7 @@ void compile_directory_mode() // Remove trailing \ if we have one. int len = strlen(full_path) - 1; - if (len > 1 && full_path[len] == '\\') + if (len > 1 && full_path[len] == DIRSEP) full_path[len] = 0; // Remove directory (should now be null). @@ -117,7 +140,7 @@ void compile_directory_mode() if (compile) { if (d->dir_name) - printf("Building %s\n", strrchr(d->dir_name, '\\') + 1); + printf("Building %s\n", strrchr(d->dir_name, DIRSEP) + 1); compile_file_list(d->dir_name, d->dir_file_list); @@ -188,10 +211,12 @@ void compile_directory_mode() timeEnd = time(NULL); printf("Elapsed time: %lld seconds\n", timeEnd - timeStart); +#ifdef BLAK_PLATFORM_WINDOWS PROCESS_MEMORY_COUNTERS pmc; GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc)); printf("Peak mem: %i bytes\n", (int)pmc.PeakWorkingSetSize); printf("Current mem: %i bytes\n",(int) pmc.WorkingSetSize); +#endif return; } @@ -210,7 +235,7 @@ void fill_lists_from_makefile(char *full_path, int recompiled_parent) char *tmpptr; int done = False; - sprintf(makefile_path, "%s\\makefile", full_path); + sprintf(makefile_path, "%s" DIRSEPSTR "makefile", full_path); makefile = fopen(makefile_path, "r"); if (makefile == NULL) { @@ -235,6 +260,8 @@ void fill_lists_from_makefile(char *full_path, int recompiled_parent) int at_bofs = False; while (fgets(temp, 256, makefile) != NULL) { + temp[strcspn(temp, "\r\n")] = '\0'; + // Our line must be greater than 6 characters if (strlen(temp) <= 6 || temp[0] == '#') continue; @@ -260,7 +287,7 @@ void fill_lists_from_makefile(char *full_path, int recompiled_parent) // Handle file. file_name[filelen] = 0; - sprintf(dir_name, "%s\\%s", full_path, file_name); + sprintf(dir_name, "%s" DIRSEPSTR "%s", full_path, file_name); int retval = recompile_check(dir_name, d, recompiled_parent); // Returns > 0 if we should check for a directory. if (retval) @@ -276,12 +303,12 @@ void fill_lists_from_makefile(char *full_path, int recompiled_parent) // Skip bof. tmpptr += 3; } - else if (*tmpptr == '\\') + else if (*tmpptr == DIRSEP || *tmpptr == '\\') { // Next line. break; } - else if (*tmpptr == '\n') + else if (*tmpptr == '\n' || *tmpptr == '\0') { done = True; break; @@ -323,7 +350,7 @@ int recompile_check(char *name, dir_data d, int recompiled_parent) // Kod must exist, others don't have to. if (stat(kodname, &time_kod) != 0) { - simple_error("Found makefile entry with missing kod file %s", strrchr(kodname,'\\') + 1); + simple_error("Found makefile entry with missing kod file %s", strrchr(kodname, DIRSEP) + 1); return False; } @@ -363,9 +390,9 @@ void dircompile_copy_files(char *bof_source, char *rsc_source, char *bofname, ch { char combine[_MAX_PATH]; - sprintf(combine, "%s\\%s", bof_output_dir, bofname); + sprintf(combine, "%s" DIRSEPSTR "%s", bof_output_dir, bofname); CopyFile(bof_source, combine, FALSE); - sprintf(combine, "%s\\%s", rsc_output_dir, rscname); + sprintf(combine, "%s" DIRSEPSTR "%s", rsc_output_dir, rscname); CopyFile(rsc_source, combine, FALSE); } diff --git a/blakcomp/kodbase.c b/blakcomp/kodbase.c index e772fcf626..d3a2b7280b 100644 --- a/blakcomp/kodbase.c +++ b/blakcomp/kodbase.c @@ -75,6 +75,7 @@ int load_kodbase(void) while (fgets(line, MAX_LINE+1, kodbase)) { kodbase_line++; + line[strcspn(line, "\r\n")] = '\0'; type_char = strtok(line," \t"); if (type_char == NULL || strlen(type_char) != 1) @@ -424,9 +425,9 @@ int build_superclasses(list_type classes) while (l != NULL) { class_type c = (class_type) l->data; - if ( (int) c->superclass != NO_SUPERCLASS) + if ( (intptr_t) c->superclass != NO_SUPERCLASS) { - superclass_idnum = (int) c->superclass; + superclass_idnum = (intptr_t) c->superclass; /* Search through classes looking for parent */ temp = classes; while (temp != NULL) diff --git a/blakcomp/makefile.linux b/blakcomp/makefile.linux index 0fed6b61b2..b9ed25fd2f 100644 --- a/blakcomp/makefile.linux +++ b/blakcomp/makefile.linux @@ -11,8 +11,8 @@ SOURCEDIR = . BLAKINCLUDEDIR += -I . -CFLAGS = -m32 -I $(BLAKINCLUDEDIR) -DBLAK_PLATFORM_LINUX -Wno-write-strings -LINKFLAGS = -m32 +CFLAGS = -I $(BLAKINCLUDEDIR) -DBLAK_PLATFORM_LINUX -Wno-write-strings +LINKFLAGS = LIBS = OBJS = \ @@ -28,6 +28,7 @@ OBJS = \ $(OUTDIR)/sort.obj \ $(OUTDIR)/optimize.obj \ $(OUTDIR)/resource.obj \ + $(OUTDIR)/dircompile.obj \ all: makedirs $(OUTDIR)/bc diff --git a/blakserv/admincons.c b/blakserv/admincons.c index b9584fbe79..8ba54b5f16 100644 --- a/blakserv/admincons.c +++ b/blakserv/admincons.c @@ -84,8 +84,9 @@ void LoadAdminConstants(void) while (fgets(line, MAX_CONSTANTS_LINE, constantsfile)) { lineno++; + line[strcspn(line, "\r\n")] = '\0'; - name_str = strtok(line, "= \t\n"); + name_str = strtok(line, "= \t"); /* ignore blank lines */ if (name_str == NULL) diff --git a/blakserv/adminfn.c b/blakserv/adminfn.c index 6558e6917a..0a569060f8 100644 --- a/blakserv/adminfn.c +++ b/blakserv/adminfn.c @@ -78,6 +78,7 @@ void AdminSaveConfiguration(int session_id, admin_parm_type parms[], int num_bla void AdminSaveOneConfigNode(config_node *c, const char *config_name, const char *default_str); void AdminWho(int session_id, admin_parm_type parms[], int num_blak_parm, parm_node blak_parm[]); void AdminWhoEachSession(session_node *s); +void AdminShutdown(int session_id, admin_parm_type parms[], int num_blak_parm, parm_node blak_parm[]); void AdminLock(int session_id, admin_parm_type parms[], int num_blak_parm, parm_node blak_parm[]); void AdminUnlock(int session_id, admin_parm_type parms[], int num_blak_parm, parm_node blak_parm[]); void AdminMail(int session_id, admin_parm_type parms[], int num_blak_parm, parm_node blak_parm[]); @@ -529,6 +530,7 @@ admin_table_type admin_main_table[] = { AdminUnlock, {N}, F, A|M, NULL, 0, "unlock", "Unlock the game" }, { NULL, {N}, F, A|M, admin_unsuspend_table, LEN_ADMIN_UNSUSPEND_TABLE,"unsuspend", "Unsuspend subcommand" }, { AdminWho, {N}, F, A|M, NULL, 0, "who", "Show every account logged on" }, + { AdminShutdown, {N}, F, A|M, NULL, 0, "shutdown", "Save game and shut down the server" }, }; #define LEN_ADMIN_MAIN_TABLE (sizeof(admin_main_table)/sizeof(admin_table_type)) @@ -4651,9 +4653,10 @@ void AdminDeleteAccount(int session_id,admin_parm_type parms[], aprintf("Account %i will be deleted.\n",a->account_id); - // XXX Need a replacement for this on Linux #ifdef BLAK_PLATFORM_WINDOWS PostThreadMessage(main_thread_id,WM_BLAK_MAIN_DELETE_ACCOUNT,0,a->account_id); +#else + DeleteAccountAndAssociatedUsersByID(a->account_id); #endif } @@ -5580,7 +5583,8 @@ void AdminRead(int session_id,admin_parm_type parms[], while (fgets(line,sizeof(line)-1,fptr)) { - ptr = strtok(line,"\n"); + line[strcspn(line, "\r\n")] = '\0'; + ptr = line; if (ptr) { while (*ptr == ' ' || *ptr == '\t') @@ -5601,9 +5605,22 @@ void AdminRead(int session_id,admin_parm_type parms[], } void AdminMark(int session_id,admin_parm_type parms[], - int num_blak_parm,parm_node blak_parm[]) + int num_blak_parm,parm_node blak_parm[]) { lprintf("-------------------------------------------------------------------------------------\n"); dprintf("-------------------------------------------------------------------------------------\n"); eprintf("-------------------------------------------------------------------------------------\n"); } + +void AdminShutdown(int session_id, admin_parm_type parms[], + int num_blak_parm, parm_node blak_parm[]) +{ + aprintf("Saving game and shutting down server...\n"); + lprintf("AdminShutdown: saving game and shutting down.\n"); + + GarbageCollect(); + SaveAll(); + + aprintf("Server shutting down now.\n"); + SetQuit(); +} diff --git a/blakserv/astar.c b/blakserv/astar.c index 9227885937..f2af130926 100644 --- a/blakserv/astar.c +++ b/blakserv/astar.c @@ -632,8 +632,14 @@ void AStarBuildEdgesCache(room_type* Room) const int cols = Room->colshighres; // iterate all squares +#ifdef BLAK_PLATFORM_WINDOWS ::concurrency::parallel_for(size_t(0), (size_t)rows, [&](size_t r) { +#else + #pragma omp parallel for schedule(dynamic) + for (size_t r = 0; r < (size_t)rows; r++) + { +#endif for (int c = 0; c < cols; c++) { // calculate index of node in edgecache @@ -743,7 +749,11 @@ void AStarBuildEdgesCache(room_type* Room) // save flags/edges Room->EdgesCache[idx] = (unsigned short)flags; } +#ifdef BLAK_PLATFORM_WINDOWS }); +#else + } +#endif } bool AStarGetStepFromCache(room_type* Room, astar_node* S, astar_node* E, V2* P, unsigned int* Flags, int ObjectID) diff --git a/blakserv/async.c b/blakserv/async.c index 205774f0cc..996e918c0d 100644 --- a/blakserv/async.c +++ b/blakserv/async.c @@ -259,15 +259,24 @@ void AsyncSocketWrite(SOCKET sock) } else { - if (bytes != bn->len_buf) - dprintf("async write wrote %i/%i bytes\n",bytes,bn->len_buf); - - transmitted_bytes += bn->len_buf; - + transmitted_bytes += bytes; + + if (bytes < bn->len_buf) + { + // Partial write - advance buffer pointer, keep buffer in send_list + bn->buf += bytes; + bn->len_buf -= bytes; + break; + } + s->send_list = bn->next; DeleteBuffer(bn); } } +#ifdef BLAK_PLATFORM_LINUX + if (s->send_list == NULL) + DisableSendEvents(s->conn.socket); +#endif if (!MutexRelease(s->muxSend)) eprintf("File %s line %i release of non-owned mutex\n",__FILE__,__LINE__); } @@ -359,7 +368,11 @@ void AsyncSocketReadUDP(SOCKET sock) SOCKADDR_IN6 senderaddr; int bytesReceivd = 0; int flags = 0; +#ifdef BLAK_PLATFORM_WINDOWS int lplen = sizeof(senderaddr); +#else + socklen_t lplen = sizeof(senderaddr); +#endif // Record time. last_udp_read_time = GetTime(); @@ -422,7 +435,11 @@ void AsyncSocketReadUDP(SOCKET sock) // 2) important: udp sender ip must match tcp session ip to prevent attacks! for (unsigned int i = 0; i < 8; i++) { +#ifdef BLAK_PLATFORM_WINDOWS if (session->conn.addr.u.Word[i] != senderaddr.sin6_addr.u.Word[i]) +#else + if (session->conn.addr.s6_addr16[i] != senderaddr.sin6_addr.s6_addr16[i]) +#endif { if (debug_udp) eprintf("AsyncSocketReadUDP warning received session-Id from different IP\n"); @@ -504,3 +521,41 @@ void AsyncSocketReadUDP(SOCKET sock) SignalSession(session->session_id); } + +#ifdef BLAK_PLATFORM_LINUX +/* AsyncSocketSelect: Dispatch socket events to the appropriate handler. + * Called from RunMainLoop in osd_epoll.c. Based on vanilla Meridian59. */ +void AsyncSocketSelect(SOCKET sock, int event, int error) +{ + session_node *s; + + EnterSessionLock(); + + if (error != 0) + { + LeaveSessionLock(); + s = GetSessionBySocket(sock); + if (s != NULL) + HangupSession(s); + return; + } + + switch (event) + { + case FD_CLOSE: + AsyncSocketClose(sock); + break; + case FD_WRITE: + AsyncSocketWrite(sock); + break; + case FD_READ: + AsyncSocketRead(sock); + break; + default: + eprintf("AsyncSocketSelect got unknown event %i\n", event); + break; + } + + LeaveSessionLock(); +} +#endif diff --git a/blakserv/async.h b/blakserv/async.h index 9905ac0c20..074acbe8a5 100644 --- a/blakserv/async.h +++ b/blakserv/async.h @@ -20,6 +20,10 @@ void AsyncNameLookup(HANDLE hLookup,int error); void ResetUDP(void); int GetLastUDPReadTime(void); +#ifdef BLAK_PLATFORM_LINUX +int AsyncSocketAccept(SOCKET sock,int event,int error,int connection_type); +void AsyncSocketSelect(SOCKET sock,int event,int error); +#endif void AsyncSocketClose(SOCKET sock); void AsyncSocketWrite(SOCKET sock); void AsyncSocketRead(SOCKET sock); diff --git a/blakserv/blakserv.h b/blakserv/blakserv.h index 7cca547d7a..d4bce061fa 100644 --- a/blakserv/blakserv.h +++ b/blakserv/blakserv.h @@ -149,7 +149,7 @@ enum #endif // BLAK_PLATFORM_WINDOWS #ifdef BLAK_PLATFORM_LINUX -#include "linux-types.h" +#include "osd_linux.h" #endif // BLAK_PLATFORM_LINUX #include @@ -163,7 +163,9 @@ enum #include #include #include +#ifdef BLAK_PLATFORM_WINDOWS #include +#endif #include "btime.h" @@ -206,11 +208,13 @@ typedef struct extern DWORD main_thread_id; void MainReloadGameData(); char * GetLastErrorStr(); +#ifdef BLAK_PLATFORM_WINDOWS #define WM_BLAK_MAIN_READ (WM_APP + 4000) #define WM_BLAK_MAIN_RECALIBRATE (WM_APP + 4001) #define WM_BLAK_MAIN_DELETE_ACCOUNT (WM_APP + 4002) #define WM_BLAK_MAIN_VERIFIED_LOGIN (WM_APP + 4003) #define WM_BLAK_MAIN_LOAD_GAME (WM_APP + 4004) +#endif #include "bof.h" @@ -297,8 +301,15 @@ char * GetLastErrorStr(); #ifdef BLAK_PLATFORM_WINDOWS #include "async_windows.h" -#else -#include "async_linux.h" +#endif + +// Linux OSD functions that need session_node/buffer_node (defined after session.h/bufpool.h) +#ifdef BLAK_PLATFORM_LINUX +void InterfaceLogon(session_node *s); +void InterfaceLogoff(session_node *s); +void InterfaceUpdateSession(session_node *s); +void InterfaceSendBufferList(buffer_node *blist); +void StartAsyncSession(session_node *s); #endif #include "debug.h" @@ -312,6 +323,8 @@ char * GetLastErrorStr(); #ifdef BLAK_PLATFORM_WINDOWS #include "database.h" +#else +#include "database_pg.h" #endif #include "jansson.h" diff --git a/blakserv/block.c b/blakserv/block.c index 6f7a1657b9..af51f63f3a 100644 --- a/blakserv/block.c +++ b/blakserv/block.c @@ -137,6 +137,7 @@ void BuildBannedIPBlocks( char *filename ) do { if(fgets(buffer,1023,fp) != NULL ) { + buffer[strcspn(buffer, "\r\n")] = '\0'; /* lets be cautious */ if (strlen(buffer)>0) { if (inet_pton(AF_INET6, buffer, &blocktoAdd) != -1) { diff --git a/blakserv/ccode.c b/blakserv/ccode.c index 6bcdaedb30..a8c1df5543 100644 --- a/blakserv/ccode.c +++ b/blakserv/ccode.c @@ -230,7 +230,11 @@ int C_LoadGame(int object_id, local_var_type *local_vars, return NIL; } +#ifdef BLAK_PLATFORM_WINDOWS MessagePost(main_thread_id, WM_BLAK_MAIN_LOAD_GAME, 0, save_time); +#else + LoadFromKod(save_time); +#endif return NIL; } @@ -3995,7 +3999,6 @@ int C_RecordStat(int object_id,local_var_type *local_vars, int num_normal_parms,parm_node normal_parm_array[], int num_name_parms,parm_node name_parm_array[]) { -#ifdef BLAK_PLATFORM_WINDOWS val_type stat_type, val, val_check; resource_node *rsc_node; session_node *session; @@ -4123,7 +4126,7 @@ int C_RecordStat(int object_id,local_var_type *local_vars, // All types okay, try insert it. MySQLRecordGeneric(stat_type.v.data, count, data); -#endif + return NIL; } @@ -4131,7 +4134,6 @@ int C_EmptyStatTable(int object_id, local_var_type *local_vars, int num_normal_parms, parm_node normal_parm_array[], int num_name_parms, parm_node name_parm_array[]) { -#ifdef BLAK_PLATFORM_WINDOWS val_type stat_type; // Don't do anything if SQL isn't enabled. @@ -4158,7 +4160,7 @@ int C_EmptyStatTable(int object_id, local_var_type *local_vars, } MySQLEmptyTable(stat_type.v.data); -#endif + return NIL; } diff --git a/blakserv/commcli.c b/blakserv/commcli.c index 32272576a3..843b6ea11d 100644 --- a/blakserv/commcli.c +++ b/blakserv/commcli.c @@ -153,7 +153,7 @@ void SecurePacketBufferList(int session_id, buffer_node *bl) session_node *s = GetSessionByID(session_id); char* pRedbook; - if (!session_id || !s || !s->account || !s->account->account_id || + if (!s || !s->account || !s->account->account_id || s->conn.type == CONN_CONSOLE) { //dprintf("SecurePacketBufferList cannot find session %i", session_id); @@ -169,8 +169,6 @@ void SecurePacketBufferList(int session_id, buffer_node *bl) return; } -// dprintf("Securing msg %u with %u", (unsigned char)bl->buf[0], (unsigned char)(s->secure_token & 0xFF)); - bl->buf[0] ^= (unsigned char)(s->secure_token & 0xFF); pRedbook = GetSecurityRedbook(); if (s->sliding_token && pRedbook) diff --git a/blakserv/config.c b/blakserv/config.c index 5ce6c5a630..2e692d81c1 100644 --- a/blakserv/config.c +++ b/blakserv/config.c @@ -642,6 +642,7 @@ void LoadConfig(void) current_group = -1; while (fgets(line,MAX_CONFIG_LINE,configfile)) { + line[strcspn(line, "\r\n")] = '\0'; current_group = LoadConfigLine(line,lineno,CONFIG_FILE,current_group); lineno++; } diff --git a/blakserv/database_pg.c b/blakserv/database_pg.c new file mode 100644 index 0000000000..48325ba359 --- /dev/null +++ b/blakserv/database_pg.c @@ -0,0 +1,955 @@ +// Meridian 59 - PostgreSQL database implementation for Linux +// Replaces MySQL database.c with libpq-based PostgreSQL support. + +#include "blakserv.h" +#include +#include + +// Worker thread state +enum { DB_STOPPED=0, DB_STOPPING, DB_STARTING, DB_CONNECTED, DB_READY }; +static int db_state = DB_STOPPED; + +// Connection +static PGconn *pg_conn = NULL; +static char *db_host = NULL; +static int db_port = 0; +static char *db_user = NULL; +static char *db_pass = NULL; +static char *db_name = NULL; + +// Queue +static sql_queue queue = { PTHREAD_MUTEX_INITIALIZER, 0, NULL, NULL }; +static UINT64 record_count = 0; + +// Worker thread +static pthread_t db_thread; + +#define MAX_RECORD_QUEUE 15000 +#define MAX_SQL_PARAMS 45 +#define DB_WORKER_SLEEP_MS 10 +#define DB_RECONNECT_SLEEP_MS 5000 +#define MAX_ITEMS_PER_LOOP 10 + +// Statistics table - maps STAT_TYPE to table name and expected field count +// timestamp_pos: 0 = no auto-timestamp, 1 = NOW() as first value, -1 = NOW() as last value +static sql_statistic_type Statistics_Table[] = { + {STAT_NONE, 0, false, NULL, 0}, + {STAT_TOTALMONEY, 1, true, "money_total", 1}, + {STAT_MONEYCREATED, 1, true, "money_created", 1}, + {STAT_PLAYERLOGIN, 3, true, "player_logins", -1}, + {STAT_ASSESS_DAM, 7, true, "player_damaged", -1, true}, + {STAT_PLAYERDEATH, 5, true, "player_death", -1}, + {STAT_PLAYER, 13, true, "player", 0}, + {STAT_PLAYERSUICIDE, 2, true, "player", 0}, + {STAT_GUILD, 4, true, "guild", 0}, + {STAT_GUILDDISBAND, 1, true, "guild", 0}, + {STAT_SPELLS, 14, true, "wiki_spells", 0}, + {STAT_SPELL_REAGENT, 3, true, "wiki_spell_reagent", 0}, + {STAT_TREASURE_GEN, 3, true, "wiki_treasure_gen", 0}, + {STAT_MONSTER, 13, true, "wiki_monster", 0}, + {STAT_MONSTER_ZONE, 3, true, "wiki_monster_zone", 0}, + {STAT_NPCS, 7, true, "wiki_npcs", 0}, + {STAT_WEAPONS, 14, true, "wiki_weapons", 0}, + {STAT_ROOMS, 6, true, "wiki_rooms", 0}, + {STAT_NPC_ZONE, 4, true, "wiki_npc_zone", 0}, + {STAT_NPC_SELLITEM, 3, true, "wiki_npc_sellitem", 0}, + {STAT_NPC_SELLSKILL, 2, true, "wiki_npc_sellskill", 0}, + {STAT_NPC_SELLSPELL, 2, true, "wiki_npc_sellspell", 0}, + {STAT_REAGENTS, 11, true, "wiki_reagents", 0}, + {STAT_FOOD, 11, true, "wiki_food", 0}, + {STAT_AMMO, 11, true, "wiki_ammo", 0}, + {STAT_ARMOR, 11, true, "wiki_armor", 0}, + {STAT_MISCITEMS, 11, true, "wiki_miscitems", 0}, + {STAT_RINGS, 11, true, "wiki_rings", 0}, + {STAT_RODS, 11, true, "wiki_rods", 0}, + {STAT_POTIONS, 11, true, "wiki_potions", 0}, + {STAT_SCROLLS, 11, true, "wiki_scrolls", 0}, + {STAT_WANDS, 11, true, "wiki_wands", 0}, + {STAT_QUESTITEMS, 11, true, "wiki_questitems", 0}, + {STAT_SKILLS, 12, true, "wiki_skills", 0}, + {STAT_NECKLACE, 11, true, "wiki_necklace", 0}, + {STAT_INSTRUMENTS, 11, true, "wiki_instruments", 0}, + {STAT_GEMS, 11, true, "wiki_gems", 0}, + {STAT_OFFERINGS, 11, true, "wiki_offerings", 0}, + {STAT_QUESTS, 11, true, "wiki_quests", 0}, + {STAT_TREASURE_EXTRA, 4, true, "wiki_treasure_extra", 0}, + {STAT_TREASURE_MAGIC, 3, true, "wiki_treasure_magic", 0}, + {STAT_NPC_SELLCOND, 4, true, "wiki_npc_sellcond", 0}, + {STAT_LOGPEN, 7, true, "player_logpen", 0}, + {STAT_LOGPEN_ITEM, 3, true, "player_logpen_items", 0}, + {STAT_QUESTGIVER, 2, true, "wiki_quest_giver", 0}, + {STAT_ACCOUNTS, 8, true, "server_accounts", 0}, + {STAT_ACCOUNT_CHARS, 2, true, "server_account_chars", 0}, + {STAT_COMPL_QUESTS, 5, true, "player_completed_quests", 0}, + {STAT_COMPL_QUEST_ITEMS,4, true, "player_quest_reward_items", 0} +}; + +/******************************************************************************/ +// Schema creation - all tables +/******************************************************************************/ + +static const char *schema_sql = +"CREATE TABLE IF NOT EXISTS money_total (" +" money_total_time TIMESTAMP NOT NULL DEFAULT NOW()," +" money_total_amount VARCHAR(18) NOT NULL);" + +"CREATE TABLE IF NOT EXISTS money_created (" +" money_created_time TIMESTAMP NOT NULL DEFAULT NOW()," +" money_created_amount INT NOT NULL);" + +"CREATE TABLE IF NOT EXISTS player_logins (" +" plogin_account_name VARCHAR(45) NOT NULL," +" plogin_character_name VARCHAR(45) NOT NULL," +" plogin_IP VARCHAR(45) NOT NULL," +" plogin_time TIMESTAMP NOT NULL DEFAULT NOW());" + +"CREATE TABLE IF NOT EXISTS player_damaged (" +" idpdamaged SERIAL PRIMARY KEY," +" pdamaged_who VARCHAR(45) NOT NULL," +" pdamaged_attacker VARCHAR(45) NOT NULL," +" pdamaged_aspell INT NOT NULL," +" pdamaged_atype INT NOT NULL," +" pdamaged_applied INT NOT NULL," +" pdamaged_original INT NOT NULL," +" pdamaged_weapon VARCHAR(45) NOT NULL," +" pdamaged_time TIMESTAMP NOT NULL DEFAULT NOW());" + +"CREATE TABLE IF NOT EXISTS player_death (" +" pdeath_victim VARCHAR(45) NOT NULL," +" pdeath_killer VARCHAR(45) NOT NULL," +" pdeath_room VARCHAR(63) NOT NULL," +" pdeath_attack VARCHAR(45) NOT NULL," +" pdeath_ispvp INT NOT NULL," +" pdeath_time TIMESTAMP NOT NULL DEFAULT NOW());" + +"CREATE TABLE IF NOT EXISTS player (" +" player_account_id INT NOT NULL," +" player_name VARCHAR(45) NOT NULL PRIMARY KEY," +" player_home VARCHAR(128)," +" player_bind VARCHAR(128)," +" player_guild VARCHAR(45)," +" player_max_health INT," +" player_max_mana INT," +" player_might INT," +" player_int INT," +" player_myst INT," +" player_stam INT," +" player_agil INT," +" player_aim INT," +" player_suicide INT DEFAULT 0," +" player_suicide_time TIMESTAMP);" + +"CREATE TABLE IF NOT EXISTS guild (" +" guild_name VARCHAR(100) NOT NULL PRIMARY KEY," +" guild_leader VARCHAR(45) NOT NULL," +" guild_hall VARCHAR(100)," +" guild_rent_paid INT NOT NULL," +" guild_disbanded INT NOT NULL DEFAULT 0," +" guild_disbanded_time TIMESTAMP);" + +"CREATE TABLE IF NOT EXISTS wiki_spells (" +" spell_id INT NOT NULL PRIMARY KEY," +" spell_name VARCHAR(63) NOT NULL," +" spell_name_ger VARCHAR(63) NOT NULL," +" spell_icon VARCHAR(63) NOT NULL," +" spell_desc TEXT," +" spell_desc_ger TEXT," +" spell_school INT," +" spell_level INT," +" spell_mana INT," +" spell_chance INT," +" spell_mediate_ratio INT," +" spell_exertion INT," +" spell_casttime INT," +" spell_iflag INT NOT NULL);" + +"CREATE TABLE IF NOT EXISTS wiki_spell_reagent (" +" spell_id INT NOT NULL," +" spell_reagent VARCHAR(63) NOT NULL," +" spell_reagent_amount INT," +" PRIMARY KEY (spell_id, spell_reagent));" + +"CREATE TABLE IF NOT EXISTS wiki_treasure_gen (" +" treasure_id INT NOT NULL," +" item_name VARCHAR(63) NOT NULL," +" item_chance INT," +" PRIMARY KEY (treasure_id, item_name));" + +"CREATE TABLE IF NOT EXISTS wiki_monster (" +" monster_name VARCHAR(63) NOT NULL PRIMARY KEY," +" monster_name_ger VARCHAR(63) NOT NULL," +" monster_icon VARCHAR(63) NOT NULL," +" monster_desc TEXT," +" monster_desc_ger TEXT," +" monster_level INT," +" monster_karma INT," +" monster_treasure INT," +" monster_speed INT," +" monster_behavior INT," +" monster_difficulty INT," +" monster_visiondistance INT," +" monster_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_monster_zone (" +" monster_rid INT NOT NULL," +" monster_name VARCHAR(63) NOT NULL," +" monster_spawnchance INT," +" PRIMARY KEY (monster_rid, monster_name));" + +"CREATE TABLE IF NOT EXISTS wiki_npcs (" +" npc_name VARCHAR(63) NOT NULL PRIMARY KEY," +" npc_name_ger VARCHAR(63) NOT NULL," +" npc_icon VARCHAR(63) NOT NULL," +" npc_desc TEXT," +" npc_desc_ger TEXT," +" npc_merchantmarkup INT," +" npc_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_weapons (" +" weapon_name VARCHAR(63) NOT NULL PRIMARY KEY," +" weapon_name_ger VARCHAR(63) NOT NULL," +" weapon_icon VARCHAR(63) NOT NULL," +" weapon_group INT NOT NULL," +" weapon_color INT NOT NULL," +" weapon_desc TEXT," +" weapon_desc_ger TEXT," +" weapon_value INT," +" weapon_weight INT," +" weapon_bulk INT," +" weapon_range INT," +" weapon_skill INT," +" weapon_prof INT," +" weapon_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_rooms (" +" room_name VARCHAR(63) NOT NULL," +" room_name_ger VARCHAR(63) NOT NULL," +" room_roo VARCHAR(63) NOT NULL," +" room_number INT NOT NULL PRIMARY KEY," +" room_region INT NOT NULL," +" room_iflag INT NOT NULL);" + +"CREATE TABLE IF NOT EXISTS wiki_npc_zone (" +" npc_name VARCHAR(63) NOT NULL," +" npc_roomid INT NOT NULL," +" npc_row INT NOT NULL," +" npc_col INT NOT NULL," +" PRIMARY KEY (npc_name, npc_roomid, npc_row, npc_col));" + +"CREATE TABLE IF NOT EXISTS wiki_npc_sellitem (" +" npc_name VARCHAR(63) NOT NULL," +" npc_item_sold VARCHAR(63) NOT NULL," +" item_color INT NOT NULL," +" PRIMARY KEY (npc_name, npc_item_sold, item_color));" + +"CREATE TABLE IF NOT EXISTS wiki_npc_sellskill (" +" npc_name VARCHAR(63) NOT NULL," +" npc_skill_id INT NOT NULL," +" PRIMARY KEY (npc_name, npc_skill_id));" + +"CREATE TABLE IF NOT EXISTS wiki_npc_sellspell (" +" npc_name VARCHAR(63) NOT NULL," +" npc_spell_id INT NOT NULL," +" PRIMARY KEY (npc_name, npc_spell_id));" + +"CREATE TABLE IF NOT EXISTS wiki_npc_sellcond (" +" npc_name VARCHAR(63) NOT NULL," +" npc_item_sold VARCHAR(63) NOT NULL," +" item_color INT NOT NULL," +" item_price INT NOT NULL," +" PRIMARY KEY (npc_name, npc_item_sold, item_color));" + +"CREATE TABLE IF NOT EXISTS wiki_reagents (" +" reagent_name VARCHAR(63) NOT NULL PRIMARY KEY," +" reagent_name_ger VARCHAR(63) NOT NULL," +" reagent_icon VARCHAR(63) NOT NULL," +" reagent_group INT NOT NULL," +" reagent_color INT NOT NULL," +" reagent_desc TEXT," +" reagent_desc_ger TEXT," +" reagent_value INT," +" reagent_weight INT," +" reagent_bulk INT," +" reagent_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_food (" +" food_name VARCHAR(63) NOT NULL PRIMARY KEY," +" food_name_ger VARCHAR(63) NOT NULL," +" food_icon VARCHAR(63) NOT NULL," +" food_group INT NOT NULL," +" food_color INT NOT NULL," +" food_desc TEXT," +" food_desc_ger TEXT," +" food_value INT," +" food_weight INT," +" food_bulk INT," +" food_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_ammo (" +" ammo_name VARCHAR(63) NOT NULL PRIMARY KEY," +" ammo_name_ger VARCHAR(63) NOT NULL," +" ammo_icon VARCHAR(63) NOT NULL," +" ammo_group INT NOT NULL," +" ammo_color INT NOT NULL," +" ammo_desc TEXT," +" ammo_desc_ger TEXT," +" ammo_value INT," +" ammo_weight INT," +" ammo_bulk INT," +" ammo_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_armor (" +" armor_name VARCHAR(63) NOT NULL PRIMARY KEY," +" armor_name_ger VARCHAR(63) NOT NULL," +" armor_icon VARCHAR(63) NOT NULL," +" armor_group INT NOT NULL," +" armor_color INT NOT NULL," +" armor_desc TEXT," +" armor_desc_ger TEXT," +" armor_value INT," +" armor_weight INT," +" armor_bulk INT," +" armor_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_miscitems (" +" misc_name VARCHAR(63) NOT NULL PRIMARY KEY," +" misc_name_ger VARCHAR(63) NOT NULL," +" misc_icon VARCHAR(63) NOT NULL," +" misc_group INT NOT NULL," +" misc_color INT NOT NULL," +" misc_desc TEXT," +" misc_desc_ger TEXT," +" misc_value INT," +" misc_weight INT," +" misc_bulk INT," +" misc_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_rings (" +" rings_name VARCHAR(63) NOT NULL PRIMARY KEY," +" rings_name_ger VARCHAR(63) NOT NULL," +" rings_icon VARCHAR(63) NOT NULL," +" rings_group INT NOT NULL," +" rings_color INT NOT NULL," +" rings_desc TEXT," +" rings_desc_ger TEXT," +" rings_value INT," +" rings_weight INT," +" rings_bulk INT," +" rings_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_rods (" +" rods_name VARCHAR(63) NOT NULL PRIMARY KEY," +" rods_name_ger VARCHAR(63) NOT NULL," +" rods_icon VARCHAR(63) NOT NULL," +" rods_group INT NOT NULL," +" rods_color INT NOT NULL," +" rods_desc TEXT," +" rods_desc_ger TEXT," +" rods_value INT," +" rods_weight INT," +" rods_bulk INT," +" rods_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_potions (" +" potion_name VARCHAR(63) NOT NULL PRIMARY KEY," +" potion_name_ger VARCHAR(63) NOT NULL," +" potion_icon VARCHAR(63) NOT NULL," +" potion_group INT NOT NULL," +" potion_color INT NOT NULL," +" potion_desc TEXT," +" potion_desc_ger TEXT," +" potion_value INT," +" potion_weight INT," +" potion_bulk INT," +" potion_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_scrolls (" +" scrolls_name VARCHAR(63) NOT NULL PRIMARY KEY," +" scrolls_name_ger VARCHAR(63) NOT NULL," +" scrolls_icon VARCHAR(63) NOT NULL," +" scrolls_group INT NOT NULL," +" scrolls_color INT NOT NULL," +" scrolls_desc TEXT," +" scrolls_desc_ger TEXT," +" scrolls_value INT," +" scrolls_weight INT," +" scrolls_bulk INT," +" scrolls_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_wands (" +" wands_name VARCHAR(63) NOT NULL PRIMARY KEY," +" wands_name_ger VARCHAR(63) NOT NULL," +" wands_icon VARCHAR(63) NOT NULL," +" wands_group INT NOT NULL," +" wands_color INT NOT NULL," +" wands_desc TEXT," +" wands_desc_ger TEXT," +" wands_value INT," +" wands_weight INT," +" wands_bulk INT," +" wands_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_questitems (" +" questitem_name VARCHAR(63) NOT NULL PRIMARY KEY," +" questitem_name_ger VARCHAR(63) NOT NULL," +" questitem_icon VARCHAR(63) NOT NULL," +" questitem_group INT NOT NULL," +" questitem_color INT NOT NULL," +" questitem_desc TEXT," +" questitem_desc_ger TEXT," +" questitem_value INT," +" questitem_weight INT," +" questitem_bulk INT," +" questitem_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_skills (" +" skill_id INT NOT NULL PRIMARY KEY," +" skill_name VARCHAR(63) NOT NULL," +" skill_name_ger VARCHAR(63) NOT NULL," +" skill_icon VARCHAR(63) NOT NULL," +" skill_desc TEXT," +" skill_desc_ger TEXT," +" skill_school INT," +" skill_level INT," +" skill_chance INT," +" skill_mediate_ratio INT," +" skill_exertion INT," +" skill_iflag INT NOT NULL);" + +"CREATE TABLE IF NOT EXISTS wiki_necklace (" +" necklace_name VARCHAR(63) NOT NULL PRIMARY KEY," +" necklace_name_ger VARCHAR(63) NOT NULL," +" necklace_icon VARCHAR(63) NOT NULL," +" necklace_group INT NOT NULL," +" necklace_color INT NOT NULL," +" necklace_desc TEXT," +" necklace_desc_ger TEXT," +" necklace_value INT," +" necklace_weight INT," +" necklace_bulk INT," +" necklace_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_instruments (" +" instrument_name VARCHAR(63) NOT NULL PRIMARY KEY," +" instrument_name_ger VARCHAR(63) NOT NULL," +" instrument_icon VARCHAR(63) NOT NULL," +" instrument_group INT NOT NULL," +" instrument_color INT NOT NULL," +" instrument_desc TEXT," +" instrument_desc_ger TEXT," +" instrument_value INT," +" instrument_weight INT," +" instrument_bulk INT," +" instrument_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_gems (" +" gem_name VARCHAR(63) NOT NULL PRIMARY KEY," +" gem_name_ger VARCHAR(63) NOT NULL," +" gem_icon VARCHAR(63) NOT NULL," +" gem_group INT NOT NULL," +" gem_color INT NOT NULL," +" gem_desc TEXT," +" gem_desc_ger TEXT," +" gem_value INT," +" gem_weight INT," +" gem_bulk INT," +" gem_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_offerings (" +" offering_name VARCHAR(63) NOT NULL PRIMARY KEY," +" offering_name_ger VARCHAR(63) NOT NULL," +" offering_icon VARCHAR(63) NOT NULL," +" offering_group INT NOT NULL," +" offering_color INT NOT NULL," +" offering_desc TEXT," +" offering_desc_ger TEXT," +" offering_value INT," +" offering_weight INT," +" offering_bulk INT," +" offering_iflag INT);" + +"CREATE TABLE IF NOT EXISTS wiki_quests (" +" quest_id INT NOT NULL PRIMARY KEY," +" quest_name VARCHAR(63) NOT NULL," +" quest_name_ger VARCHAR(63) NOT NULL," +" quest_icon VARCHAR(63) NOT NULL," +" quest_icon_group INT NOT NULL," +" quest_desc TEXT," +" quest_desc_ger TEXT," +" quest_recent_time INT," +" quest_schedule_pct INT," +" quest_est_time INT," +" quest_est_diff INT);" + +"CREATE TABLE IF NOT EXISTS wiki_treasure_extra (" +" treasure_id INT NOT NULL," +" item_name VARCHAR(63) NOT NULL," +" item_min_amount INT," +" item_max_amount INT," +" PRIMARY KEY (treasure_id, item_name));" + +"CREATE TABLE IF NOT EXISTS wiki_treasure_magic (" +" treasure_id INT NOT NULL," +" item_name VARCHAR(63) NOT NULL," +" item_attribute_id INT NOT NULL," +" PRIMARY KEY (treasure_id, item_name, item_attribute_id));" + +"CREATE TABLE IF NOT EXISTS player_logpen (" +" player_name VARCHAR(63) NOT NULL," +" logpen_time TIMESTAMP NOT NULL DEFAULT NOW()," +" room_id INT," +" logpen_xp INT NOT NULL," +" logpen_hp INT NOT NULL," +" logpen_spellpct INT NOT NULL," +" logpen_skillpct INT NOT NULL," +" logpen_numitems INT NOT NULL);" + +"CREATE TABLE IF NOT EXISTS player_logpen_items (" +" logpen_item_id SERIAL PRIMARY KEY," +" player_name VARCHAR(63) NOT NULL," +" logpen_time TIMESTAMP NOT NULL DEFAULT NOW()," +" item_name VARCHAR(63) NOT NULL);" + +"CREATE TABLE IF NOT EXISTS player_completed_quests (" +" quest_id INT NOT NULL," +" player_name VARCHAR(63) NOT NULL," +" completion_time TIMESTAMP NOT NULL DEFAULT NOW()," +" num_seconds_taken INT NOT NULL," +" xp_rewarded INT NOT NULL);" + +"CREATE TABLE IF NOT EXISTS player_quest_reward_items (" +" quest_rewarditem_id SERIAL PRIMARY KEY," +" quest_id INT NOT NULL," +" player_name VARCHAR(63) NOT NULL," +" completion_time TIMESTAMP NOT NULL DEFAULT NOW()," +" item_name VARCHAR(63) NOT NULL);" + +"CREATE TABLE IF NOT EXISTS wiki_quest_giver (" +" quest_id INT NOT NULL," +" quest_npc_name VARCHAR(63) NOT NULL," +" PRIMARY KEY (quest_id, quest_npc_name));" + +"CREATE TABLE IF NOT EXISTS server_accounts (" +" acct_id INT NOT NULL PRIMARY KEY," +" acct_name VARCHAR(63) NOT NULL," +" acct_password VARCHAR(63) NOT NULL," +" acct_email VARCHAR(255) NOT NULL," +" acct_type INT NOT NULL," +" acct_loggedin_time INT NOT NULL," +" acct_last_login INT NOT NULL," +" acct_suspend_time INT NOT NULL);" + +"CREATE TABLE IF NOT EXISTS server_account_chars (" +" entry_id SERIAL PRIMARY KEY," +" acct_id INT NOT NULL," +" char_name VARCHAR(63) NOT NULL);" +; + +/******************************************************************************/ +// Internal functions +/******************************************************************************/ + +static bool PgConnect(void) +{ + char conninfo[512]; + snprintf(conninfo, sizeof(conninfo), + "host=%s port=%d user=%s password=%s dbname=%s", + db_host, db_port, db_user, db_pass, db_name); + + pg_conn = PQconnectdb(conninfo); + if (PQstatus(pg_conn) != CONNECTION_OK) + { + eprintf("PostgreSQL connection failed: %s\n", PQerrorMessage(pg_conn)); + PQfinish(pg_conn); + pg_conn = NULL; + return false; + } + + // Set client encoding to LATIN1 for German umlauts and legacy data + PQsetClientEncoding(pg_conn, "LATIN1"); + + lprintf("PostgreSQL connected to %s:%d/%s\n", db_host, db_port, db_name); + return true; +} + +static bool PgVerifySchema(void) +{ + PGresult *res = PQexec(pg_conn, schema_sql); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + eprintf("PostgreSQL schema creation failed: %s\n", PQerrorMessage(pg_conn)); + PQclear(res); + return false; + } + PQclear(res); + dprintf("PostgreSQL schema verified\n"); + return true; +} + +static bool PgIsConnected(void) +{ + if (!pg_conn) + return false; + if (PQstatus(pg_conn) != CONNECTION_OK) + { + // Try to reset the connection + PQreset(pg_conn); + return PQstatus(pg_conn) == CONNECTION_OK; + } + return true; +} + +static void PgWriteNode(sql_queue_node *node) +{ + sql_data_node *data = (sql_data_node *)node->data; + int type = node->type; + int nf = node->num_fields; + + if (type <= STAT_NONE || type > STAT_MAXSTAT) + return; + + if (!Statistics_Table[type].enabled) + return; + + const char *table = Statistics_Table[type].table_name; + if (!table) + return; + + // Build parameterized INSERT + // Convert all fields to string params for PQexecParams + const char *paramValues[MAX_SQL_PARAMS]; + char numbufs[MAX_SQL_PARAMS][20]; + int nParams = 0; + + for (int i = 0; i < nf && i < MAX_SQL_PARAMS; i++) + { + switch (data[i].type) + { + case TAG_NIL: + paramValues[nParams] = NULL; + break; + case TAG_INT: + snprintf(numbufs[nParams], sizeof(numbufs[0]), "%d", data[i].value.num); + paramValues[nParams] = numbufs[nParams]; + break; + case TAG_STRING: + case TAG_RESOURCE: + paramValues[nParams] = data[i].value.str ? data[i].value.str : ""; + break; + case TAG_SESSION: + snprintf(numbufs[nParams], sizeof(numbufs[0]), "%d", data[i].value.num); + paramValues[nParams] = numbufs[nParams]; + break; + default: + paramValues[nParams] = NULL; + break; + } + nParams++; + } + + // Build INSERT query with $1, $2, ... placeholders + // Handle auto-columns: SERIAL (DEFAULT) and TIMESTAMP (NOW()) + int ts_pos = Statistics_Table[type].timestamp_pos; + bool has_serial = Statistics_Table[type].has_serial; + char sql[4096]; + char placeholders[1024]; + placeholders[0] = 0; + + if (has_serial) + strcat(placeholders, "DEFAULT,"); + + if (ts_pos == 1) + strcat(placeholders, "NOW(),"); + + for (int i = 0; i < nParams; i++) + { + char ph[8]; + snprintf(ph, sizeof(ph), "%s$%d", i > 0 ? "," : "", i + 1); + strcat(placeholders, ph); + } + + if (ts_pos == -1) + strcat(placeholders, ",NOW()"); + + snprintf(sql, sizeof(sql), "INSERT INTO %s VALUES (%s) ON CONFLICT DO NOTHING", table, placeholders); + + PGresult *res = PQexecParams(pg_conn, sql, nParams, NULL, + paramValues, NULL, NULL, 0); + + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + // Don't spam errors - just log once + static int error_count = 0; + if (error_count++ < 10) + eprintf("PostgreSQL INSERT into %s failed: %s\n", table, PQerrorMessage(pg_conn)); + } + else + { + record_count++; + } + PQclear(res); +} + +static void PgTruncateTable(int type) +{ + if (type <= STAT_NONE || type > STAT_MAXSTAT) + return; + + const char *table = Statistics_Table[type].table_name; + if (!table) + return; + + char sql[256]; + snprintf(sql, sizeof(sql), "TRUNCATE TABLE %s", table); + PGresult *res = PQexec(pg_conn, sql); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + eprintf("PostgreSQL TRUNCATE %s failed: %s\n", table, PQerrorMessage(pg_conn)); + PQclear(res); +} + +static bool PgDequeue(void) +{ + pthread_mutex_lock(&queue.mutex); + if (queue.count == 0) + { + pthread_mutex_unlock(&queue.mutex); + return false; + } + + sql_queue_node *node = queue.first; + queue.first = node->next; + if (queue.first == NULL) + queue.last = NULL; + queue.count--; + pthread_mutex_unlock(&queue.mutex); + + if (node->num_fields < 0) + PgTruncateTable(node->type); + else + PgWriteNode(node); + + // Free data + if (node->data) + FreeDataNodeMemory(abs(node->num_fields), + abs(node->num_fields), (sql_data_node *)node->data); + FreeMemory(MALLOC_ID_SQL, node, sizeof(sql_queue_node)); + + return true; +} + +static void *PgWorker(void *arg) +{ + db_state = DB_STARTING; + + // Connect + while (db_state != DB_STOPPING) + { + if (PgConnect()) + { + if (PgVerifySchema()) + { + db_state = DB_READY; + break; + } + PQfinish(pg_conn); + pg_conn = NULL; + } + // Retry after delay + usleep(DB_RECONNECT_SLEEP_MS * 1000); + } + + // Main work loop + while (db_state != DB_STOPPING) + { + if (db_state == DB_READY && PgIsConnected()) + { + int processed = 0; + for (int i = 0; i < MAX_ITEMS_PER_LOOP; i++) + { + if (PgDequeue()) + processed++; + else + break; + } + if (processed == 0) + usleep(DB_WORKER_SLEEP_MS * 1000); + } + else + { + // Try reconnect + if (!PgIsConnected()) + { + eprintf("PostgreSQL connection lost, reconnecting...\n"); + if (!PgConnect()) + usleep(DB_RECONNECT_SLEEP_MS * 1000); + else + db_state = DB_READY; + } + } + } + + // Drain remaining queue + while (PgDequeue()) {} + + if (pg_conn) + { + PQfinish(pg_conn); + pg_conn = NULL; + } + db_state = DB_STOPPED; + return NULL; +} + +/******************************************************************************/ +// Public API +/******************************************************************************/ + +void MySQLInit(char *Host, int Port, char *User, char *Password, char *DB) +{ + if (db_state != DB_STOPPED) + return; + + if (!Host || !User || !Password || !DB) + { + eprintf("PostgreSQL init failed: missing connection parameters\n"); + return; + } + + db_host = strdup(Host); + db_port = Port; + db_user = strdup(User); + db_pass = strdup(Password); + db_name = strdup(DB); + + pthread_create(&db_thread, NULL, PgWorker, NULL); + usleep(100000); // 100ms for thread startup +} + +void MySQLEnd() +{ + if (db_state == DB_STOPPED) + return; + + db_state = DB_STOPPING; + pthread_join(db_thread, NULL); + + free(db_host); db_host = NULL; + free(db_user); db_user = NULL; + free(db_pass); db_pass = NULL; + free(db_name); db_name = NULL; +} + +BOOL MySQLRecordGeneric(int type, int num_fields, sql_data_node data[]) +{ + if (type <= STAT_NONE || type > STAT_MAXSTAT) + return FALSE; + + if (!Statistics_Table[type].enabled) + { + FreeDataNodeMemory(num_fields, num_fields, data); + return FALSE; + } + + if (db_state < DB_READY) + { + FreeDataNodeMemory(num_fields, num_fields, data); + return FALSE; + } + + sql_queue_node *node = (sql_queue_node *)AllocateMemory(MALLOC_ID_SQL, + sizeof(sql_queue_node)); + node->type = (sql_recordtype)type; + node->num_fields = num_fields; + node->data = data; + node->next = NULL; + + pthread_mutex_lock(&queue.mutex); + if (queue.count >= MAX_RECORD_QUEUE) + { + pthread_mutex_unlock(&queue.mutex); + FreeDataNodeMemory(num_fields, num_fields, data); + FreeMemory(MALLOC_ID_SQL, node, sizeof(sql_queue_node)); + eprintf("PostgreSQL queue full, dropping record\n"); + return FALSE; + } + + if (queue.last) + queue.last->next = node; + else + queue.first = node; + queue.last = node; + queue.count++; + pthread_mutex_unlock(&queue.mutex); + + return TRUE; +} + +BOOL MySQLEmptyTable(int type) +{ + sql_queue_node *node = (sql_queue_node *)AllocateMemory(MALLOC_ID_SQL, + sizeof(sql_queue_node)); + node->type = (sql_recordtype)type; + node->num_fields = -1; + node->data = NULL; + node->next = NULL; + + pthread_mutex_lock(&queue.mutex); + if (queue.last) + queue.last->next = node; + else + queue.first = node; + queue.last = node; + queue.count++; + pthread_mutex_unlock(&queue.mutex); + + return TRUE; +} + +UINT64 MySQLGetRecordCount() +{ + return record_count; +} + +char *MySQLDuplicateString(char *str) +{ + if (!str) + { + bprintf("MySQLDuplicateString could not duplicate null string!"); + return NULL; + } + int sLen = strlen(str); + char *ret_str = (char *)AllocateMemory(MALLOC_ID_SQL, sLen + 1); + memcpy(ret_str, str, sLen); + ret_str[sLen] = 0; + return ret_str; +} + +bool MySQLIsTypeEnabled(int type) +{ + if (type >= STAT_NONE && type <= STAT_MAXSTAT) + return Statistics_Table[type].enabled; + return false; +} + +bool MySQLSetTypeEnabled(int type, bool enabled) +{ + if (type >= STAT_NONE && type <= STAT_MAXSTAT) + { + Statistics_Table[type].enabled = enabled; + return true; + } + return false; +} + +void FreeDataNodeMemory(int total_fields, int fields_entered, sql_data_node data[]) +{ + for (int i = 0; i < fields_entered; i++) + { + if ((data[i].type == TAG_STRING || data[i].type == TAG_RESOURCE) + && data[i].value.str) + { + FreeMemory(MALLOC_ID_SQL, data[i].value.str, strlen(data[i].value.str) + 1); + } + } + FreeMemory(MALLOC_ID_SQL, data, sizeof(sql_data_node) * total_fields); +} diff --git a/blakserv/database_pg.h b/blakserv/database_pg.h new file mode 100644 index 0000000000..a77b306aa2 --- /dev/null +++ b/blakserv/database_pg.h @@ -0,0 +1,115 @@ +// Meridian 59 - PostgreSQL database implementation for Linux +// Replaces MySQL database.h/database.c with libpq-based PostgreSQL support. +// Same public API as the MySQL version for seamless integration. + +#ifndef _DATABASE_PG_H +#define _DATABASE_PG_H + +#include + +#pragma region Structs/Enums + +enum sql_recordtype +{ + STAT_NONE = 0, + STAT_TOTALMONEY = 1, + STAT_MONEYCREATED = 2, + STAT_PLAYERLOGIN = 3, + STAT_ASSESS_DAM = 4, + STAT_PLAYERDEATH = 5, + STAT_PLAYER = 6, + STAT_PLAYERSUICIDE = 7, + STAT_GUILD = 8, + STAT_GUILDDISBAND = 9, + STAT_SPELLS = 10, + STAT_SPELL_REAGENT = 11, + STAT_TREASURE_GEN = 12, + STAT_MONSTER = 13, + STAT_MONSTER_ZONE = 14, + STAT_NPCS = 15, + STAT_WEAPONS = 16, + STAT_ROOMS = 17, + STAT_NPC_ZONE = 18, + STAT_NPC_SELLITEM = 19, + STAT_NPC_SELLSKILL = 20, + STAT_NPC_SELLSPELL = 21, + STAT_REAGENTS = 22, + STAT_FOOD = 23, + STAT_AMMO = 24, + STAT_ARMOR = 25, + STAT_MISCITEMS = 26, + STAT_RINGS = 27, + STAT_RODS = 28, + STAT_POTIONS = 29, + STAT_SCROLLS = 30, + STAT_WANDS = 31, + STAT_QUESTITEMS = 32, + STAT_SKILLS = 33, + STAT_NECKLACE = 34, + STAT_INSTRUMENTS = 35, + STAT_GEMS = 36, + STAT_OFFERINGS = 37, + STAT_QUESTS = 38, + STAT_TREASURE_EXTRA = 39, + STAT_TREASURE_MAGIC = 40, + STAT_NPC_SELLCOND = 41, + STAT_LOGPEN = 42, + STAT_LOGPEN_ITEM = 43, + STAT_QUESTGIVER = 44, + STAT_ACCOUNTS = 45, + STAT_ACCOUNT_CHARS = 46, + STAT_COMPL_QUESTS = 47, + STAT_COMPL_QUEST_ITEMS = 48, + STAT_MAXSTAT = 48 +}; + +typedef enum sql_recordtype sql_recordtype; + +struct sql_data_node +{ + int type; + union { + int num; + char *str; + } value; +}; + +struct sql_queue_node +{ + sql_recordtype type; + int num_fields; + void *data; + sql_queue_node *next; +}; + +struct sql_queue +{ + pthread_mutex_t mutex; + int count; + sql_queue_node *first; + sql_queue_node *last; +}; + +// timestamp_pos: 0 = no auto-timestamp, 1 = NOW() as first value, -1 = NOW() as last value +// has_serial: true if table has SERIAL PRIMARY KEY as first column +typedef struct { + sql_recordtype stat_type; + int num_fields; + bool enabled; + const char *table_name; + int timestamp_pos; + bool has_serial; +} sql_statistic_type; + +// Public API - same names as MySQL version for compatibility +void MySQLInit(char *Host, int Port, char *User, char *Password, char *DB); +void MySQLEnd(); +BOOL MySQLRecordGeneric(int type, int num_fields, sql_data_node data[]); +BOOL MySQLEmptyTable(int type); +UINT64 MySQLGetRecordCount(); +char *MySQLDuplicateString(char *str); +bool MySQLIsTypeEnabled(int type); +bool MySQLSetTypeEnabled(int type, bool enabled); +void FreeDataNodeMemory(int total_fields, int fields_entered, sql_data_node data[]); + +#endif diff --git a/blakserv/dllist.c b/blakserv/dllist.c index 02762bc655..268cdc2279 100644 --- a/blakserv/dllist.c +++ b/blakserv/dllist.c @@ -213,8 +213,9 @@ void AddBuiltInDLlist() while (fgets(line,MAX_PACKAGE_LINE,packagefile)) { lineno++; + line[strcspn(line, "\r\n")] = '\0'; - t1 = strtok(line," \t\n"); + t1 = strtok(line," \t"); if (t1 == NULL) /* ignore blank lines */ continue; diff --git a/blakserv/files.c b/blakserv/files.c index 56bb0d4946..fdd541ed64 100644 --- a/blakserv/files.c +++ b/blakserv/files.c @@ -40,6 +40,8 @@ bool FindMatchingFiles(const char *path, std::vector *files) std::string sext = spath.substr(last_found+2); spath = spath.substr(0,last_found); + files->clear(); + DIR *dir = opendir(spath.c_str()); if (dir == NULL) return false; @@ -58,7 +60,11 @@ bool FindMatchingFiles(const char *path, std::vector *files) } closedir(dir); - + + std::sort(files->begin(), files->end(), + [](const std::string &a, const std::string &b) { + return strcasecmp(a.c_str(), b.c_str()) < 0; + }); return true; #else diff --git a/blakserv/game.c b/blakserv/game.c index 28d523d1a3..aa00c98d0b 100644 --- a/blakserv/game.c +++ b/blakserv/game.c @@ -606,7 +606,6 @@ void GameStartUser(session_node *s,user_node *u) name_val.int_val = SendTopLevelBlakodMessage(s->game->object_id,USER_NAME_MSG,0,NULL); r = GetResourceByID(name_val.v.data); -#ifdef BLAK_PLATFORM_WINDOWS if (r && r->resource_val[0]) { // Strings and sql_data_node freed in database.c. @@ -619,7 +618,6 @@ void GameStartUser(session_node *s,user_node *u) data[2].value.str = MySQLDuplicateString(s->conn.name); MySQLRecordGeneric(STAT_PLAYERLOGIN, 3, data); } -#endif SetSessionTimer(s, SESSION_POLL_TIME); } diff --git a/blakserv/garbage.c b/blakserv/garbage.c index 2ee638d84b..59da765967 100644 --- a/blakserv/garbage.c +++ b/blakserv/garbage.c @@ -108,7 +108,12 @@ void GarbageCollect() so we can ignore messages from before this GC */ if (GetKodStats()) + { GetKodStats()->interpreting_time_object_id = INVALID_ID; + GetKodStats()->interpreting_time_message_id = INVALID_ID; + GetKodStats()->interpreting_time_highest = 0; + GetKodStats()->interpreting_time_posts = 0; + } // Tables now get GC'd, so don't reset them. //ResetTables(); diff --git a/blakserv/interface_linux.c b/blakserv/interface_linux.c index 89c57a6f2b..8c99445489 100644 --- a/blakserv/interface_linux.c +++ b/blakserv/interface_linux.c @@ -8,99 +8,93 @@ #include "blakserv.h" -int sessions_logged_on; -int console_session_id; -pthread_t interface_thread; +static int sessions_logged_on = 0; +static int console_session_id = INVALID_ID; +static pthread_t interface_thread; #define ADMIN_RESPONSE_SIZE (256 * 1024) -char admin_response_buf[ADMIN_RESPONSE_SIZE+1]; -int len_admin_response_buf; +static char admin_response_buf[ADMIN_RESPONSE_SIZE+1]; +static int len_admin_response_buf; -void InterfaceAddAdminBuffer(char *buf,int len_buf); +static void InterfaceAddAdminBuffer(char *buf, int len_buf); +// InitInterface: called late (after StartupComplete) only in interactive mode. +// Creates the console admin session and starts the console input thread. void InitInterface(void) { - int err; + connection_node conn; + session_node *s; - err = pthread_create(&interface_thread, NULL, &InterfaceMainLoop, NULL); + len_admin_response_buf = 0; + + conn.type = CONN_CONSOLE; + s = CreateSession(conn); + if (s == NULL) + FatalError("Interface can't make session for console"); + s->account = GetConsoleAccount(); + InitSessionState(s, STATE_ADMIN); + console_session_id = s->session_id; + int err = pthread_create(&interface_thread, NULL, &InterfaceMainLoop, NULL); if (err != 0) - { - eprintf("Unable to start interface! %s",strerror(err)); - } + eprintf("Unable to start interface thread: %s\n", strerror(err)); else - { - dprintf("interface thread started"); - } + dprintf("Interface console thread started\n"); } void* InterfaceMainLoop(void* arg) { char *line = (char*) malloc(200); size_t size; - //char buf[200]; - while (strcmp(line,"quit") != 0) + for (;;) { printf("blakadm> "); - if (getline(&line, &size, stdin) != -1) - { - - EnterServerLock(); + fflush(stdout); + if (getline(&line, &size, stdin) == -1) + break; + + // Strip trailing newline from getline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; - // TODO: in windows this uses a set char array of size 200, no bounds checking - // is being done here yet - //TryAdminCommand(console_session_id,buf); - TryAdminCommand(console_session_id,line); + if (strcmp(line, "quit") == 0) + break; - LeaveServerLock(); + if (strlen(line) == 0) + continue; - } + EnterServerLock(); + TryAdminCommand(console_session_id, line); + LeaveServerLock(); } free(line); - MessagePost(main_thread_id,WM_QUIT,0,0); + SetQuit(); + + return NULL; } -void StartupPrintf(const char *fmt,...) +void StartupPrintf(const char *fmt, ...) { char s[200]; va_list marker; - va_start(marker,fmt); - vsprintf(s,fmt,marker); + va_start(marker, fmt); + vsnprintf(s, sizeof(s), fmt, marker); + va_end(marker); if (strlen(s) > 0) { - if (s[strlen(s)-1] == '\n') /* ignore \n char at the end of line */ - s[strlen(s)-1] = 0; + if (s[strlen(s)-1] == '\n') + s[strlen(s)-1] = 0; } - va_end(marker); - - // TODO: Actually print something to stdout + printf("Startup: %s\n", s); } -// XXX: identical to windows version -void StartupComplete(void) -{ - char str[200]; - connection_node conn; - session_node *s; - - len_admin_response_buf = 0; - - conn.type = CONN_CONSOLE; - s = CreateSession(conn); - if (s == NULL) - FatalError("Interface can't make session for console"); - s->account = GetConsoleAccount(); - InitSessionState(s,STATE_ADMIN); - console_session_id = s->session_id; -} - -// XXX: identical to windows version int GetUsedSessions(void) { return sessions_logged_on; @@ -108,31 +102,26 @@ int GetUsedSessions(void) void InterfaceUpdate(void) { - // TODO: stub } void InterfaceLogon(session_node *s) { - // TODO: stub + sessions_logged_on++; } void InterfaceLogoff(session_node *s) { - // TODO: stub + sessions_logged_on--; } void InterfaceUpdateSession(session_node *s) { - // TODO: stub } void InterfaceUpdateChannel(void) { - // TODO: stub } -// called in main thread -// XXX: identical to windows version void InterfaceSendBufferList(buffer_node *blist) { buffer_node *bn; @@ -140,42 +129,36 @@ void InterfaceSendBufferList(buffer_node *blist) bn = blist; while (bn != NULL) { - InterfaceAddAdminBuffer(bn->buf,bn->len_buf); + InterfaceAddAdminBuffer(bn->buf, bn->len_buf); bn = bn->next; } DeleteBufferList(blist); } -// called in main thread -// XXX: identical to windows version -void InterfaceSendBytes(char *buf,int len_buf) +void InterfaceSendBytes(char *buf, int len_buf) { - InterfaceAddAdminBuffer(buf,len_buf); + InterfaceAddAdminBuffer(buf, len_buf); } -// in main thread called by InterfaceSendBytes -void InterfaceAddAdminBuffer(char *buf,int len_buf) +static void InterfaceAddAdminBuffer(char *buf, int len_buf) { if (len_buf > ADMIN_RESPONSE_SIZE) len_buf = ADMIN_RESPONSE_SIZE; if (len_admin_response_buf + len_buf > ADMIN_RESPONSE_SIZE) - { len_admin_response_buf = 0; - } - memcpy(admin_response_buf+len_admin_response_buf,buf,len_buf); + + memcpy(admin_response_buf + len_admin_response_buf, buf, len_buf); len_admin_response_buf += len_buf; admin_response_buf[len_admin_response_buf] = 0; - // supposedly writing to stdout is thread safe although the threads - // are going to clobber each others output. there is probably a better - // way but for now output is sent directly to stdout - printf(admin_response_buf); + printf("%s", admin_response_buf); + len_admin_response_buf = 0; } -void FatalErrorShow(const char *filename,int line,const char *str) +void FatalErrorShow(const char *filename, int line, const char *str) { - // TODO: stub + fprintf(stderr, "FATAL ERROR: File %s line %i\n%s\n", filename, line, str); + exit(1); } - diff --git a/blakserv/interface_linux.h b/blakserv/interface_linux.h index 243a1093a6..9212b345a6 100644 --- a/blakserv/interface_linux.h +++ b/blakserv/interface_linux.h @@ -15,7 +15,6 @@ void InitInterface(void); void* InterfaceMainLoop(void*); void StartupPrintf(const char *fmt,...); -void StartupComplete(void); int GetUsedSessions(void); diff --git a/blakserv/intrlock.c b/blakserv/intrlock.c index 3d159ae29f..b78a66bd74 100644 --- a/blakserv/intrlock.c +++ b/blakserv/intrlock.c @@ -49,9 +49,12 @@ void LeaveServerLock() void SetQuit() { - EnterCriticalSection(&csQuit); + EnterCriticalSection(&csQuit); quit = True; +#ifdef BLAK_PLATFORM_WINDOWS MessagePost(main_thread_id,WM_QUIT,0,0); +#endif + // On Linux, RunMainLoop checks GetQuit() each iteration LeaveCriticalSection(&csQuit); } @@ -68,5 +71,8 @@ Bool GetQuit() void SignalSession(int session_id) { +#ifdef BLAK_PLATFORM_WINDOWS MessagePost(main_thread_id,WM_BLAK_MAIN_READ,0,session_id); +#endif + // On Linux, sessions are polled in RunMainLoop via PollSessions() } diff --git a/blakserv/kodbase.c b/blakserv/kodbase.c index ebe1cd5e01..5834e4b5e3 100644 --- a/blakserv/kodbase.c +++ b/blakserv/kodbase.c @@ -52,7 +52,6 @@ void LoadKodbase(void) while (fgets(line, MAX_LINE, kodbase)) { lineno++; - type_char = strtok(line, " \t"); if (type_char == NULL || strlen(type_char) != 1) { diff --git a/blakserv/linux-types.h b/blakserv/linux-types.h index 64ad19f06d..1ee3cf766f 100644 --- a/blakserv/linux-types.h +++ b/blakserv/linux-types.h @@ -1,3 +1,4 @@ +#include #include #include #include @@ -35,7 +36,7 @@ typedef unsigned char BYTE; typedef unsigned short WORD; #define MAKEWORD(low, high) ((WORD)((((WORD)(high)) << 8) | ((BYTE)(low)))) -typedef unsigned long DWORD; +typedef uint32_t DWORD; typedef long long INT64; typedef void* PVOID; typedef void* LPVOID; @@ -47,7 +48,7 @@ typedef unsigned int UINT; typedef bool BOOL; #define TRUE 1 #define FALSE 0 -typedef unsigned long LPARAM; +typedef uint32_t LPARAM; typedef unsigned int WPARAM; typedef const char* LPCSTR; typedef LPCSTR LPCTSTR; diff --git a/blakserv/loadacco.c b/blakserv/loadacco.c index 32bc3eea7f..d20ec25954 100644 --- a/blakserv/loadacco.c +++ b/blakserv/loadacco.c @@ -59,8 +59,9 @@ Bool LoadAccounts(char *filename) while (fgets(line, MAX_ACCOUNT_LINE, accofile)) { lineno++; + line[strcspn(line, "\r\n")] = '\0'; - type_str = strtok(line, ": \t\n"); + type_str = strtok(line, ": \t"); if (type_str == NULL) /* ignore blank lines */ continue; diff --git a/blakserv/loadall.c b/blakserv/loadall.c index c888c7383c..ed22cddea2 100644 --- a/blakserv/loadall.c +++ b/blakserv/loadall.c @@ -176,9 +176,10 @@ Bool LoadControlFile(int *last_save_time) while (fgets(line,MAX_SAVE_CONTROL_LINE,loadfile)) { lineno++; - - t1 = strtok(line," \n"); - t2 = strtok(NULL," \n"); + line[strcspn(line, "\r\n")] = '\0'; + + t1 = strtok(line," "); + t2 = strtok(NULL," "); if (t1 == NULL) /* ignore blank lines */ continue; diff --git a/blakserv/main.c b/blakserv/main.c index dea8509c62..3365e509f0 100644 --- a/blakserv/main.c +++ b/blakserv/main.c @@ -21,9 +21,11 @@ #include "blakserv.h" /* local function prototypes */ -void MainUsage(); +void MainServer(void); +void MainExitServer(void); DWORD main_thread_id; +static bool in_main_loop = false; #ifdef BLAK_PLATFORM_WINDOWS @@ -32,32 +34,203 @@ int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_ char *command_line, _In_ int how_show) { - main_thread_id = GetCurrentThreadId(); - - StoreInstanceData(hInstance,how_show); - - MainServer(); - - return 0; + main_thread_id = GetCurrentThreadId(); + StoreInstanceData(hInstance, how_show); + MainServer(); + return 0; } #else +static bool interactive_mode = false; + +static void Daemonize(void) +{ + pid_t pid; + + pid = fork(); + if (pid < 0) + { + fprintf(stderr, "Error: failed to fork\n"); + exit(1); + } + if (pid > 0) + exit(0); // parent exits + + setsid(); + + int fd = open("/dev/null", O_RDWR, 0); + if (fd != -1) + { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > 2) + close(fd); + } +} + int main(int argc, char **argv) { - main_thread_id = pthread_self(); + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "-i") == 0) + interactive_mode = true; + } - return MainServer(argc,argv); + MainServer(); + return 0; } #endif -// This function keeps the necessary calls to reset and reinit game data -// in one place, to reduce the chance of errors if modifying it. Used by -// the interface and admin reload commands. -void MainReloadGameData() +#ifdef BLAK_PLATFORM_LINUX +Bool InMainLoop(void) +{ + return in_main_loop; +} + +// On Windows, MainServer() is in main_windows.c +// On Linux, it's here with RunMainLoop() instead of ServiceTimers() +void MainServer(void) +{ + InitInterfaceLocks(); + + if (!interactive_mode) + Daemonize(); + + InitMemory(); + InitConfig(); + LoadConfig(); + + InitDebug(); + InitChannelBuffer(); + OpenDefaultChannels(); + + if (ConfigBool(MYSQL_ENABLED)) + { +#ifdef BLAK_PLATFORM_WINDOWS + lprintf("Starting MySQL writer\n"); +#else + lprintf("Starting PostgreSQL writer\n"); +#endif + MySQLInit(ConfigStr(MYSQL_HOST), ConfigInt(MYSQL_CPORT), ConfigStr(MYSQL_USERNAME), + ConfigStr(MYSQL_PASSWORD), ConfigStr(MYSQL_DB)); + } + + lprintf("Starting %s\n", BlakServLongVersionString()); + + InitClass(); + InitMessage(); + InitObject(); + InitList(); + InitTimer(); + InitSession(); + InitResource(); + AStarInit(); + InitRooms(); + InitString(); + InitUser(); + InitAccount(); + InitNameID(); + InitDLlist(); + InitSysTimer(); + InitMotd(); + InitLoadBof(); + InitTime(); + InitGameLock(); + InitBkodInterpret(); + InitBufferPool(); + InitTables(); + AddBuiltInDLlist(); + LoadMotd(); + LoadBof(); + LoadRsc(); + LoadKodbase(); + LoadAdminConstants(); + PauseTimers(); + + if (LoadAll() == True) + { + SendTopLevelBlakodMessage(GetSystemObjectID(), LOADED_GAME_MSG, 0, NULL); + DoneLoadAccounts(); + } + + InitCommCli(); + InitParseClient(); + InitProfiling(); + InitAsyncConnections(); + UpdateSecurityRedbook(); + UnpauseTimers(); + + StartupComplete(); + + if (interactive_mode) + { + InitInterface(); + printf("%s\n", BlakServLongVersionString()); + printf("Status: %i accounts, %i timers\n", GetNextAccountID(), GetNumActiveTimers()); + } + + InterfaceUpdate(); + + lprintf("Status: %i accounts\n", GetNextAccountID()); + lprintf("-----------------------------------------------------------------------------------------------\n"); + dprintf("-----------------------------------------------------------------------------------------------\n"); + eprintf("-----------------------------------------------------------------------------------------------\n"); + gprintf("-----------------------------------------------------------------------------------------------\n"); + + in_main_loop = true; + + AsyncSocketStart(); + + if (interactive_mode) + printf("Server ready. Type 'quit' to shut down.\n"); + +#ifdef BLAK_PLATFORM_WINDOWS + SetWindowText(hwndMain, ConfigStr(CONSOLE_CAPTION)); + ServiceTimers(); +#else + RunMainLoop(); +#endif + + MainExitServer(); +} + +void MainExitServer(void) +{ + lprintf("ExitServer terminating server\n"); + + ExitAsyncConnections(); + MySQLEnd(); + CloseAllSessions(); + CloseDefaultChannels(); + + ResetLoadMotd(); + ResetLoadBof(); + ResetTables(); + ResetBufferPool(); + ResetSysTimer(); + ResetDLlist(); + ResetNameID(); + ResetAccount(); + ResetUser(); + ResetString(); + ExitRooms(); + AStarShutdown(); + ResetResource(); + ResetTimer(); + ResetList(); + ResetObject(); + ResetMessage(); + ResetClass(); + ResetConfig(); + DeleteAllBlocks(); +} +#endif // BLAK_PLATFORM_LINUX + +void MainReloadGameData(void) { - // Reset data. ResetAdminConstants(); ResetUser(); ResetString(); @@ -74,7 +247,6 @@ void MainReloadGameData() ResetMessage(); ResetClass(); - // Reload data. InitNameID(); LoadMotd(); LoadBof(); @@ -85,22 +257,16 @@ void MainReloadGameData() LoadAdminConstants(); } +#ifdef BLAK_PLATFORM_WINDOWS char * GetLastErrorStr() { -#ifdef BLAK_PLATFORM_WINDOWS + char *error_str; - char *error_str; - - error_str = "No error string"; /* in case the call fails */ - - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - NULL,GetLastError(),MAKELANGID(LANG_ENGLISH,SUBLANG_ENGLISH_US), - (LPTSTR) &error_str,0,NULL); - return error_str; - -#else + error_str = "No error string"; - return strerror(errno); - -#endif + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + NULL,GetLastError(),MAKELANGID(LANG_ENGLISH,SUBLANG_ENGLISH_US), + (LPTSTR) &error_str,0,NULL); + return error_str; } +#endif diff --git a/blakserv/makefile.linux b/blakserv/makefile.linux index 8b6fbdd3d0..0f0fedd153 100644 --- a/blakserv/makefile.linux +++ b/blakserv/makefile.linux @@ -1,10 +1,10 @@ # -# makefile for blakserv +# makefile for blakserv (Linux) +# Based on vanilla Meridian59 approach: single-threaded with epoll # TOPDIR=.. -#!include $(TOPDIR)/common.mak CP = cp RM = rm MKDIR = mkdir @@ -16,21 +16,18 @@ BLAKINCLUDEDIR = $(TOPDIR)/include BLAKSERVRUNDIR = $(TOPDIR)/run/server # -g generate debug info for gdb -# -m32 compile as 32-bit -# -Wno-write-strings: turns off deprecated warning for const char* -# strings passed as char* (Should be properly fixed) -CFLAGS = -m32 -I $(BLAKINCLUDEDIR) -g -x c++ -DBLAK_PLATFORM_LINUX -Wno-write-strings +# -msse2 for SSE2 intrinsics (astar.c) +CFLAGS = -msse2 -I $(BLAKINCLUDEDIR) -I /usr/include/postgresql -g -x c++ \ + -DBLAK_PLATFORM_LINUX -Wno-write-strings -Wno-unused-result -fopenmp - -BLAKSERVLINKFLAGS = -m32 -lpthread -lrt +BLAKSERVLINKFLAGS = -lpthread -lrt -lpq -fopenmp SOURCEDIR = . LIBS = -OBJS = \ +OBJS = \ $(OUTDIR)/main.obj \ - $(OUTDIR)/main_linux.obj \ $(OUTDIR)/loadkod.obj \ $(OUTDIR)/class.obj \ $(OUTDIR)/message.obj \ @@ -49,7 +46,6 @@ OBJS = \ $(OUTDIR)/commcli.obj \ $(OUTDIR)/string.obj \ $(OUTDIR)/async.obj \ - $(OUTDIR)/async_linux.obj \ $(OUTDIR)/loadgame.obj \ $(OUTDIR)/game.obj \ $(OUTDIR)/term.obj \ @@ -97,11 +93,11 @@ OBJS = \ $(OUTDIR)/intstringhash.obj \ $(OUTDIR)/files.obj \ $(OUTDIR)/sprocket.obj \ - $(OUTDIR)/linux-critical_section.obj \ + $(OUTDIR)/astar.obj \ + $(OUTDIR)/osd_linux.obj \ + $(OUTDIR)/osd_epoll.obj \ $(OUTDIR)/interface_linux.obj \ - $(OUTDIR)/mutex_linux.obj \ - $(OUTDIR)/thdmsgqueue_linux.obj \ - $(OUTDIR)/astar.obj \ + $(OUTDIR)/database_pg.obj all : makedirs $(OUTDIR)/blakserv @@ -116,12 +112,9 @@ $(OUTDIR)/md5.obj : $(TOPDIR)/util/md5.c $(CC) $(CFLAGS) -o $@ -c $< $(OUTDIR)/blakserv: $(OBJS) -# $(CC) $(CFLAGS) -o $@ -c $(SOURCEDIR)/version.c -# $(LINK) $@ $(LIBS) $(BLAKSERVLINKFLAGS) $(LINKFLAGS) $(LINK) -o $@ $(OBJS) $(BLAKSERVLINKFLAGS) $(LINKFLAGS) $(CP) $@ $(BLAKSERVRUNDIR) -#!include $(TOPDIR)/rules.mak makedirs: -$(MKDIR) -p $(OUTDIR) diff --git a/blakserv/osd_epoll.c b/blakserv/osd_epoll.c new file mode 100644 index 0000000000..343e993c4f --- /dev/null +++ b/blakserv/osd_epoll.c @@ -0,0 +1,365 @@ +// Meridian 59, Copyright 1994-2012 Andrew Kirmse and Chris Kirmse. +// All rights reserved. +// +// This software is distributed under a license that is described in +// the LICENSE file that accompanies it. +// +// Meridian is a registered trademark. +/* + * osd_epoll.c + * + * Linux networking and main loop based on epoll(). + * Based on vanilla Meridian59, extended with UDP support. + */ + +#include "blakserv.h" +#include +#include + +int fd_epoll; +static int fd_wakeup = -1; // eventfd for timer wakeup + +#define MAX_MAINTENANCE_MASKS 15 +static char *maintenance_masks[MAX_MAINTENANCE_MASKS]; +static int num_maintenance_masks = 0; +static char *maintenance_buffer = NULL; + +Bool CheckMaintenanceMask(SOCKADDR_IN6 *addr, int len_addr); + +static INT64 GetMainLoopWaitTime(void) +{ + INT64 ms; + int numActive = GetNumActiveTimers(); + + if (numActive == 0) + ms = 500; + else + { + ms = GetNextTimerTime() - GetMilliCount(); + if (ms <= 0) + ms = 0; + if (ms > 500) + ms = 500; + } + return ms; +} + +void RunMainLoop(void) +{ + INT64 ms; + const uint32_t num_notify_events = 500; + struct epoll_event notify_events[num_notify_events]; + int i; + + signal(SIGPIPE, SIG_IGN); + signal(SIGTERM, [](int) { SetQuit(); }); + signal(SIGINT, [](int) { SetQuit(); }); + + dprintf("RunMainLoop started on epoll fd %d\n", fd_epoll); + + while (!GetQuit()) + { + ms = GetMainLoopWaitTime(); + + int val = epoll_wait(fd_epoll, notify_events, num_notify_events, ms); + if (val == -1) + { + if (errno != EINTR) + eprintf("RunMainLoop error on epoll_wait %s\n", GetLastErrorStr()); + continue; + } + + for (i = 0; i < val; i++) + { + if (notify_events[i].events == 0) + continue; + + // Timer wakeup eventfd - just drain it and let TimerActivate run + if (notify_events[i].data.fd == fd_wakeup) + { + uint64_t val; + read(fd_wakeup, &val, sizeof(val)); + continue; + } + + // UDP socket handling + if (notify_events[i].data.fd == GetUDPSocket()) + { + if (notify_events[i].events & EPOLLIN) + AsyncSocketReadUDP(notify_events[i].data.fd); + continue; + } + + if (IsAcceptingSocket(notify_events[i].data.fd)) + { + if (notify_events[i].events & ~EPOLLIN) + { + eprintf("RunMainLoop error on accepting socket %i\n", + notify_events[i].data.fd); + } + else + { + EnterServerLock(); + AsyncSocketAccept(notify_events[i].data.fd, FD_ACCEPT, 0, + GetAcceptingSocketConnectionType(notify_events[i].data.fd)); + LeaveServerLock(); + } + } + else + { + if (notify_events[i].events & ~(EPOLLIN | EPOLLOUT)) + { + AsyncSocketSelect(notify_events[i].data.fd, 0, 1); + } + else + { + if (notify_events[i].events & EPOLLIN) + AsyncSocketSelect(notify_events[i].data.fd, FD_READ, 0); + if (notify_events[i].events & EPOLLOUT) + AsyncSocketSelect(notify_events[i].data.fd, FD_WRITE, 0); + } + } + } + + EnterServerLock(); + PollSessions(); + TimerActivate(); + LeaveServerLock(); + } + + close(fd_epoll); +} + +void StartupComplete(void) +{ + fd_epoll = epoll_create(1); + + // Create eventfd for timer wakeup - allows TimerAddNode to + // wake the epoll loop immediately when a new earliest timer is added. + fd_wakeup = eventfd(0, EFD_NONBLOCK); + if (fd_wakeup >= 0) + { + struct epoll_event ee; + ee.events = EPOLLIN; + ee.data.fd = fd_wakeup; + if (epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_wakeup, &ee) != 0) + eprintf("StartupComplete error adding wakeup eventfd: %s\n", GetLastErrorStr()); + else + dprintf("Timer wakeup eventfd initialized (fd=%d)\n", fd_wakeup); + } + else + eprintf("StartupComplete error creating wakeup eventfd: %s\n", GetLastErrorStr()); +} + +void WakeupMainLoop(void) +{ + if (fd_wakeup >= 0) + { + uint64_t val = 1; + write(fd_wakeup, &val, sizeof(val)); + } +} + +void StartAsyncSocketAccept(SOCKET sock, int connection_type) +{ + struct epoll_event ee; + ee.events = EPOLLIN; + ee.data.fd = sock; + if (epoll_ctl(fd_epoll, EPOLL_CTL_ADD, sock, &ee) != 0) + { + eprintf("StartAsyncSocketAccept error adding socket %s\n", GetLastErrorStr()); + return; + } + AddAcceptingSocket(sock, connection_type); +} + +void StartAsyncSession(session_node *s) +{ + struct epoll_event ee; + ee.events = EPOLLIN; + ee.data.fd = s->conn.socket; + if (epoll_ctl(fd_epoll, EPOLL_CTL_ADD, s->conn.socket, &ee) != 0) + { + eprintf("StartAsyncSession error adding socket %s\n", GetLastErrorStr()); + return; + } +} + +void EnableSendEvents(SOCKET sock) +{ + struct epoll_event ee; + ee.events = EPOLLIN | EPOLLOUT; + ee.data.fd = sock; + epoll_ctl(fd_epoll, EPOLL_CTL_MOD, sock, &ee); +} + +void DisableSendEvents(SOCKET sock) +{ + struct epoll_event ee; + ee.events = EPOLLIN; + ee.data.fd = sock; + epoll_ctl(fd_epoll, EPOLL_CTL_MOD, sock, &ee); +} + +void StartAsyncSocketUDPRead(SOCKET sock) +{ + struct epoll_event ee; + ee.events = EPOLLIN; + ee.data.fd = sock; + if (epoll_ctl(fd_epoll, EPOLL_CTL_ADD, sock, &ee) != 0) + { + eprintf("StartAsyncSocketUDPRead error adding UDP socket %s\n", GetLastErrorStr()); + return; + } + SetUDPSocket(sock); +} + +void ExitAsyncConnections(void) +{ +} + +void InitAsyncConnections(void) +{ + maintenance_buffer = (char *)malloc(strlen(ConfigStr(SOCKET_MAINTENANCE_MASK)) + 1); + strcpy(maintenance_buffer, ConfigStr(SOCKET_MAINTENANCE_MASK)); + + maintenance_masks[num_maintenance_masks] = strtok(maintenance_buffer, ";"); + while (maintenance_masks[num_maintenance_masks] != NULL) + { + num_maintenance_masks++; + if (num_maintenance_masks == MAX_MAINTENANCE_MASKS) + break; + maintenance_masks[num_maintenance_masks] = strtok(NULL, ";"); + } +} + +int AsyncSocketAccept(SOCKET sock, int event, int error, int connection_type) +{ + SOCKET new_sock; + SOCKADDR_IN6 acc_sin; + socklen_t acc_sin_len; + SOCKADDR_IN6 peer_info; + socklen_t peer_len; + struct in6_addr peer_addr; + connection_node conn; + session_node *s; + + if (event != FD_ACCEPT) + { + eprintf("AsyncSocketAccept got non-accept %i\n", event); + return 0; + } + + if (error != 0) + { + eprintf("AsyncSocketAccept got error %i\n", error); + return 0; + } + + acc_sin_len = sizeof(acc_sin); + new_sock = accept(sock, (struct sockaddr *)&acc_sin, &acc_sin_len); + + if (new_sock == SOCKET_ERROR) + { + if (errno != EAGAIN && errno != EWOULDBLOCK) + eprintf("AsyncSocketAccept accept failed, error %i\n", GetLastError()); + return SOCKET_ERROR; + } + + peer_len = sizeof(peer_info); + if (getpeername(new_sock, (SOCKADDR *)&peer_info, &peer_len) < 0) + { + eprintf("AsyncSocketAccept getpeername failed error %i\n", GetLastError()); + return 0; + } + + memcpy(&peer_addr, &peer_info.sin6_addr, sizeof(struct in6_addr)); + memcpy(&conn.addr, &peer_addr, sizeof(struct in6_addr)); + inet_ntop(AF_INET6, &peer_addr, conn.name, sizeof(conn.name)); + + if (connection_type == SOCKET_MAINTENANCE_PORT) + { + if (!CheckMaintenanceMask(&peer_info, peer_len)) + { + lprintf("Blocked maintenance connection from %s.\n", conn.name); + closesocket(new_sock); + return 0; + } + } + else + { + if (!CheckBlockList(&peer_addr)) + { + lprintf("Blocked connection from %s.\n", conn.name); + closesocket(new_sock); + return 0; + } + } + + conn.type = CONN_SOCKET; + conn.socket = new_sock; + + // Set TCP_NODELAY and non-blocking on accepted socket + // Linux accept() does not inherit socket options from listen socket + { + int nodelay = 1; + int flags; + setsockopt(new_sock, IPPROTO_TCP, TCP_NODELAY, (char *)&nodelay, sizeof(nodelay)); + flags = fcntl(new_sock, F_GETFL, 0); + if (fcntl(new_sock, F_SETFL, flags | O_NONBLOCK) < 0) + eprintf("AsyncSocketAccept error setting non-blocking on socket\n"); + } + + s = CreateSession(conn); + if (s != NULL) + { + StartAsyncSession(s); + + switch (connection_type) + { + case SOCKET_PORT: + InitSessionState(s, STATE_SYNCHED); + break; + case SOCKET_MAINTENANCE_PORT: + InitSessionState(s, STATE_MAINTENANCE); + break; + default: + eprintf("AsyncSocketAccept got invalid connection type %i\n", connection_type); + } + + s->conn.hLookup = 0; + } + + return new_sock; +} + +Bool CheckMaintenanceMask(SOCKADDR_IN6 *addr, int len_addr) +{ + IN6_ADDR mask; + + for (int i = 0; i < num_maintenance_masks; i++) + { + if (inet_pton(AF_INET6, maintenance_masks[i], &mask) != 1) + { + eprintf("CheckMaintenanceMask has invalid configured mask %s\n", + maintenance_masks[i]); + continue; + } + + BOOL skip = 0; + for (int k = 0; k < (int)sizeof(mask.s6_addr); k++) + { + if (mask.s6_addr[k] != 0 && mask.s6_addr[k] != addr->sin6_addr.s6_addr[k]) + { + skip = 1; + break; + } + } + + if (skip) + continue; + + return True; + } + return False; +} diff --git a/blakserv/osd_linux.c b/blakserv/osd_linux.c new file mode 100644 index 0000000000..c16cf1fc7a --- /dev/null +++ b/blakserv/osd_linux.c @@ -0,0 +1,142 @@ +// Meridian 59, Copyright 1994-2012 Andrew Kirmse and Chris Kirmse. +// All rights reserved. +// +// This software is distributed under a license that is described in +// the LICENSE file that accompanies it. +// +// Meridian is a registered trademark. +/* + * osd_linux.c + * + * OS-dependent function implementations for Linux. + * Based on vanilla Meridian59, extended for our codebase. + */ + +#include "blakserv.h" + +#include +#include + +typedef std::pair fd_conn_type; +static std::vector accept_sockets; + +// Track UDP socket separately +static SOCKET udp_socket = INVALID_SOCKET; + +bool IsAcceptingSocket(int sock) +{ + for (auto it = accept_sockets.begin(); it != accept_sockets.end(); ++it) + { + if (it->first == sock) + return true; + } + return false; +} + +int GetAcceptingSocketConnectionType(int sock) +{ + for (auto it = accept_sockets.begin(); it != accept_sockets.end(); ++it) + { + if (it->first == sock) + return it->second; + } + return 0; +} + +void AddAcceptingSocket(int sock, int connection_type) +{ + accept_sockets.push_back(std::make_pair(sock, connection_type)); +} + +SOCKET GetUDPSocket(void) +{ + return udp_socket; +} + +void SetUDPSocket(SOCKET sock) +{ + udp_socket = sock; +} + +int GetLastError() +{ + return errno; +} + +char *GetLastErrorStr() +{ + return strerror(errno); +} + +HANDLE StartAsyncNameLookup(char *peer_addr, char *buf) +{ + // DNS reverse lookup not implemented on Linux + return 0; +} + +// Mutex implementation using pthread recursive mutex +void InitializeCriticalSection(CRITICAL_SECTION *cs) +{ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(cs, &attr); + pthread_mutexattr_destroy(&attr); +} + +void DeleteCriticalSection(CRITICAL_SECTION *cs) +{ + pthread_mutex_destroy(cs); +} + +void EnterCriticalSection(CRITICAL_SECTION *cs) +{ + pthread_mutex_lock(cs); +} + +void LeaveCriticalSection(CRITICAL_SECTION *cs) +{ + pthread_mutex_unlock(cs); +} + +Mutex MutexCreate(void) +{ + pthread_mutex_t *m = new pthread_mutex_t; + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(m, &attr); + pthread_mutexattr_destroy(&attr); + return m; +} + +bool MutexAcquire(Mutex mutex) +{ + return pthread_mutex_lock(mutex) == 0; +} + +bool MutexAcquireWithTimeout(Mutex mutex, int timeoutMs) +{ + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += timeoutMs / 1000; + ts.tv_nsec += (timeoutMs % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) + { + ts.tv_nsec -= 1000000000L; + ts.tv_sec += 1; + } + return pthread_mutex_timedlock(mutex, &ts) == 0; +} + +bool MutexRelease(Mutex mutex) +{ + return pthread_mutex_unlock(mutex) == 0; +} + +bool MutexClose(Mutex mutex) +{ + pthread_mutex_destroy(mutex); + delete mutex; + return true; +} diff --git a/blakserv/osd_linux.h b/blakserv/osd_linux.h new file mode 100644 index 0000000000..d2df9211e3 --- /dev/null +++ b/blakserv/osd_linux.h @@ -0,0 +1,130 @@ +// Meridian 59, Copyright 1994-2012 Andrew Kirmse and Chris Kirmse. +// All rights reserved. +// +// This software is distributed under a license that is described in +// the LICENSE file that accompanies it. +// +// Meridian is a registered trademark. +/* + * osd_linux.h + * + * OS-dependent type definitions and function declarations for Linux. + * Based on vanilla Meridian59's osd_linux.h, extended with IPv6 and UDP support. + */ + +#ifndef _OSD_LINUX_H +#define _OSD_LINUX_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_PATH PATH_MAX +#define O_BINARY 0 +#define O_TEXT 0 +#define stricmp strcasecmp +#define strnicmp strncasecmp + +// MSVC-specific keywords +#define __forceinline inline __attribute__((always_inline)) +#define _MM_ALIGN16 __attribute__((aligned(16))) +#define ZeroMemory(dest, size) memset((dest), 0, (size)) + +// Socket compatibility +typedef int SOCKET; +#define closesocket close +#define SOCKET_ERROR -1 +#define INVALID_SOCKET -1 +#define WSAEWOULDBLOCK EWOULDBLOCK +#define WSAGetLastError() errno +#define SOCKADDR_IN6 sockaddr_in6 +#define SOCKADDR sockaddr +#define IN6_ADDR in6_addr +#define FD_CLOSE 32 +#define FD_ACCEPT 8 +#define FD_READ 1 +#define FD_WRITE 2 + +// Windows type compatibility +typedef unsigned char BYTE; +typedef unsigned short WORD; +#define MAKEWORD(low, high) ((WORD)((((WORD)(high)) << 8) | ((BYTE)(low)))) +typedef uint32_t DWORD; +typedef long long INT64; +typedef void* PVOID; +typedef void* LPVOID; +typedef PVOID HANDLE; +typedef HANDLE HINSTANCE; +typedef HANDLE HWND; +typedef unsigned long long UINT64; +typedef unsigned int UINT; +typedef bool BOOL; +#define TRUE 1 +#define FALSE 0 +typedef uint32_t LPARAM; +typedef unsigned int WPARAM; +typedef const char* LPCSTR; +typedef LPCSTR LPCTSTR; +#define INVALID_HANDLE_VALUE ((HANDLE)-1) + +#define VER_PLATFORM_WIN32_WINDOWS 1 +#define VER_PLATFORM_WIN32_NT 2 +#define PROCESSOR_INTEL_386 386 +#define PROCESSOR_INTEL_486 486 +#define PROCESSOR_INTEL_PENTIUM 586 + +#define MAXGETHOSTSTRUCT 64 + +// Mutex type - uses pthread recursive mutex +typedef pthread_mutex_t* Mutex; +Mutex MutexCreate(void); +bool MutexAcquire(Mutex mutex); +bool MutexAcquireWithTimeout(Mutex mutex, int timeoutMs); +bool MutexRelease(Mutex mutex); +bool MutexClose(Mutex mutex); + +// Critical section compatibility (maps to mutex) +typedef pthread_mutex_t CRITICAL_SECTION; +void InitializeCriticalSection(CRITICAL_SECTION *cs); +void DeleteCriticalSection(CRITICAL_SECTION *cs); +void EnterCriticalSection(CRITICAL_SECTION *cs); +void LeaveCriticalSection(CRITICAL_SECTION *cs); + +// OSD function declarations (types that don't need forward decls) +int GetLastError(); +char *GetLastErrorStr(); + +void RunMainLoop(void); +void EnableSendEvents(SOCKET sock); +void DisableSendEvents(SOCKET sock); +void WakeupMainLoop(void); + +void StartupComplete(void); + +void StartAsyncSocketAccept(SOCKET sock,int connection_type); +void StartAsyncSocketUDPRead(SOCKET sock); +HANDLE StartAsyncNameLookup(char *peer_addr,char *buf); + +void FatalErrorShow(const char *filename,int line,const char *str); +#define FatalError(a) FatalErrorShow(__FILE__,__LINE__,a) + +bool IsAcceptingSocket(int sock); +int GetAcceptingSocketConnectionType(int sock); +void AddAcceptingSocket(int sock, int connection_type); + +SOCKET GetUDPSocket(void); +void SetUDPSocket(SOCKET sock); + +#endif diff --git a/blakserv/sendmsg.c b/blakserv/sendmsg.c index d8dbd8f7d5..1282f68837 100644 --- a/blakserv/sendmsg.c +++ b/blakserv/sendmsg.c @@ -372,7 +372,8 @@ int SendTopLevelBlakodMessage(int object_id,int message_id,int num_parms,parm_no if (message_depth != 0) { eprintf("SendTopLevelBlakodMessage called with message_depth %i " - "and message id %i\n", message_depth,message_id); + "and message id %i, resetting to 0\n", message_depth,message_id); + message_depth = 0; } start_time = GetMicroCountDouble(); @@ -431,7 +432,8 @@ int SendTopLevelBlakodMessage(int object_id,int message_id,int num_parms,parm_no if (message_depth != 0) { eprintf("SendTopLevelBlakodMessage returning with message_depth %i " - "and message id %i\n", message_depth, message_id); + "and message id %i, resetting to 0\n", message_depth, message_id); + message_depth = 0; } return ret_val; diff --git a/blakserv/session.c b/blakserv/session.c index a64b549bf0..415f1daa1d 100644 --- a/blakserv/session.c +++ b/blakserv/session.c @@ -982,10 +982,10 @@ void SendGameClientBufferList(session_node *s,buffer_node *blist,char seqno) buffer_node *bn; unsigned short crc16; unsigned int len; - + if (blist == NULL) return; - + len = 0; bn = blist; while (bn != NULL) @@ -993,28 +993,19 @@ void SendGameClientBufferList(session_node *s,buffer_node *blist,char seqno) len += bn->len_buf; bn = bn->next; } - + /* dprintf("SendClientBufferList %i bytes\n",len); */ - + crc16 = GetCRC16BufferList(blist); - - + memcpy(blist->prebuf,&len,LENBYTES); memcpy(blist->prebuf + LENBYTES,&crc16,CRCBYTES); memcpy(blist->prebuf + LENBYTES + CRCBYTES,&len,LENBYTES); blist->prebuf[LENBYTES*2 + CRCBYTES] = seqno; - + blist->buf = blist->prebuf; blist->len_buf += HEADERBYTES; - /* - bn = NULL; - bn = AddToBufferList(bn,&len,LENBYTES); - bn = AddToBufferList(bn,&crc16,CRCBYTES); - bn = AddToBufferList(bn,&len,LENBYTES); - bn = AddToBufferList(bn,&seqno,1); - - bn->next = blist; - */ + SendBufferList(s,blist); } @@ -1044,10 +1035,11 @@ void SendBufferList(session_node *s,buffer_node *blist) if (s->send_list == NULL) { /* if nothing in queue, try to send right now */ - + while (blist != NULL) { - if (send(s->conn.socket,blist->buf,blist->len_buf,0) == SOCKET_ERROR) + int bytes = send(s->conn.socket,blist->buf,blist->len_buf,0); + if (bytes == SOCKET_ERROR) { if (GetLastError() != WSAEWOULDBLOCK) { @@ -1061,16 +1053,31 @@ void SendBufferList(session_node *s,buffer_node *blist) /* dprintf("%i adding to buffer list\n",s->session_id); */ SessionAddBufferList(s,blist); +#ifdef BLAK_PLATFORM_LINUX + EnableSendEvents(s->conn.socket); +#endif break; } else { - transmitted_bytes += blist->len_buf; - + transmitted_bytes += bytes; + + if (bytes < blist->len_buf) + { + /* Partial write - advance buffer, queue remainder */ + blist->buf += bytes; + blist->len_buf -= bytes; + SessionAddBufferList(s,blist); +#ifdef BLAK_PLATFORM_LINUX + EnableSendEvents(s->conn.socket); +#endif + break; + } + bn = blist->next; DeleteBuffer(blist); blist = bn; - } + } } } else diff --git a/blakserv/synched.c b/blakserv/synched.c index f47982a381..4c5f6c262a 100644 --- a/blakserv/synched.c +++ b/blakserv/synched.c @@ -477,6 +477,8 @@ void VerifyLogin(session_node *s) /* they're logged in now. Check their version number, and if old tell 'em to update it */ cli_vers = s->version_major * 100 + s->version_minor; + dprintf("VERSION CHECK: cli_vers=%d invalid=%d classic_min=%d\n", + cli_vers, ConfigInt(LOGIN_INVALID_VERSION), ConfigInt(LOGIN_CLASSIC_MIN_VERSION)); if (cli_vers < ConfigInt(LOGIN_INVALID_VERSION)) { SendSynchedMessage(s,ConfigStr(LOGIN_INVALID_VERSION_STR),LA_LOGOFF); @@ -497,6 +499,7 @@ void VerifyLogin(session_node *s) else if (s->version_minor <= 40) SynchedSendClientPatchOldClassic(s); else + dprintf("SENDING PATCH: classic version too old\n"); SynchedSendClientPatchClassic(s); #endif InterfaceUpdateSession(s); @@ -525,12 +528,16 @@ void VerifyLogin(session_node *s) else { str = GetRsbMD5(); + dprintf("RSB hash check: server='%s' client='%s'\n", str, s->rsb_hash); if (*str != 0 && s->rsb_hash[0] != 0 && strcmp(s->rsb_hash, str) != 0) { if (s->version_major == 50) + { + dprintf("SENDING PATCH: RSB hash mismatch\n"); SynchedSendClientPatchClassic(s); + } else if (s->version_major == 90) SynchedSendClientPatchOgre(s); InterfaceUpdateSession(s); @@ -538,7 +545,9 @@ void VerifyLogin(session_node *s) SetSessionTimer(s, 60 * ConfigInt(INACTIVE_TRANSFER)); } else + { SynchedDoMenu(s); + } } } diff --git a/blakserv/time.c b/blakserv/time.c index 92de4a03a5..a5080a6e8d 100644 --- a/blakserv/time.c +++ b/blakserv/time.c @@ -64,9 +64,17 @@ const char * TimeStr(time_t time) return "Invalid Time"; if (tm_time->tm_mday < 10) +#ifdef BLAK_PLATFORM_LINUX + time_format = "%b %-d %Y %H:%M:%S"; +#else time_format = "%b %#d %Y %H:%M:%S"; +#endif else +#ifdef BLAK_PLATFORM_LINUX + time_format = "%b %-d %Y %H:%M:%S"; +#else time_format = "%b %#d %Y %H:%M:%S"; +#endif if (strftime(s,sizeof(s),time_format,tm_time) == 0) return "Time string too long"; @@ -86,9 +94,17 @@ const char * ShortTimeStr(time_t time) tm_time = localtime(&time); if (tm_time->tm_mday < 10) +#ifdef BLAK_PLATFORM_LINUX + time_format = "%b %-d %H:%M"; +#else time_format = "%b %#d %H:%M"; +#endif else +#ifdef BLAK_PLATFORM_LINUX + time_format = "%b %-d %H:%M"; +#else time_format = "%b %#d %H:%M"; +#endif if (strftime(s,sizeof(s),time_format,tm_time) == 0) return "Time string too long"; diff --git a/blakserv/timer.c b/blakserv/timer.c index a4136a41d3..5bcb980331 100644 --- a/blakserv/timer.c +++ b/blakserv/timer.c @@ -19,7 +19,11 @@ #define RCHILD(x) (2 * x + 2) #define PARENT(x) ((x-1)/2) +#ifdef BLAK_PLATFORM_WINDOWS static Bool in_main_loop = False; +#else +extern bool in_main_loop; +#endif static int numActiveTimers = 0; // Min binary heap array of pointers of timer_node. @@ -120,7 +124,11 @@ __forceinline void TimerAddNode(timer_node *t) { // We're making a new first-timer, so the time main loop should wait might // have changed, so have it break out of loop and recalibrate +#ifdef BLAK_PLATFORM_WINDOWS MessagePost(main_thread_id, WM_BLAK_MAIN_RECALIBRATE, 0, 0); +#elif defined(BLAK_PLATFORM_LINUX) + WakeupMainLoop(); +#endif } // Start node off at end of heap. @@ -364,6 +372,7 @@ void TimerActivate() } } +#ifdef BLAK_PLATFORM_WINDOWS Bool InMainLoop(void) { return in_main_loop; @@ -467,6 +476,14 @@ void ServiceTimers(void) } } } +#endif // BLAK_PLATFORM_WINDOWS + +INT64 GetNextTimerTime(void) +{ + if (numActiveTimers == 0) + return 0; + return timer_heap[0]->time; +} // Iterate through timer_heap array and compare timer_id. timer_node * GetTimerByID(int timer_id) diff --git a/blakserv/timer.h b/blakserv/timer.h index f949d89108..e5b9d65a45 100644 --- a/blakserv/timer.h +++ b/blakserv/timer.h @@ -43,5 +43,7 @@ void ForEachTimerMatchingObjID(void (*callback_func)(timer_node *t), int o_id); void SetNumTimers(int new_next_timer_num); Bool InMainLoop(void); int GetNumActiveTimers(void); +INT64 GetNextTimerTime(void); +void TimerActivate(void); #endif diff --git a/blakserv/user.c b/blakserv/user.c index 6c2366d6ab..ebfe4651da 100644 --- a/blakserv/user.c +++ b/blakserv/user.c @@ -64,7 +64,11 @@ user_node * CreateNewUser(int account_id,int class_id) p[0].value = system_id_const.int_val; p[0].name_id = SYSTEM_PARM; +#ifdef BLAK_PLATFORM_WINDOWS sprintf(buf,"User%i%i%I64i",account_id,GetTime()%100000,GetMilliCount()%1000); +#else + sprintf(buf,"User%i%i%llu",account_id,GetTime()%100000,GetMilliCount()%1000); +#endif name_val.v.tag = TAG_RESOURCE; name_val.v.data = AddDynamicResource(buf); diff --git a/include/md5.h b/include/md5.h index b9cf61e3a4..7af6cdc081 100644 --- a/include/md5.h +++ b/include/md5.h @@ -37,7 +37,8 @@ typedef unsigned char *POINTER; typedef unsigned short int UINT2; /* UINT4 defines a four byte word */ -typedef unsigned long int UINT4; +#include +typedef uint32_t UINT4; void MDString (char *string, unsigned char *digest); void MDFileHash(char *string, char *filehash, unsigned int bytes); diff --git a/kod/kod.mak.linux b/kod/kod.mak.linux index 763bef8b6e..b716ff8844 100644 --- a/kod/kod.mak.linux +++ b/kod/kod.mak.linux @@ -16,7 +16,7 @@ BCFLAGS = -d -I $(KODINCLUDEDIR) -K $(KODDIR)/kodbase.txt fi all : $(BOFS) $(BOFS2) $(BOFS3) $(BOFS4) $(BOFS5) $(BOFS6) $(BOFS7) $(BOFS8) - @for i in $(BOFS:.bof=) $(BOFS2:.bof=) $(BOFS3:.bof=.) $(BOFS4:.bof=.) $(BOFS5:.bof=.) $(BOFS6:.bof=.) $(BOFS7:.bof=.) $(BOFS8:.bof=.); do \ + @for i in $(BOFS:.bof=) $(BOFS2:.bof=) $(BOFS3:.bof=) $(BOFS4:.bof=) $(BOFS5:.bof=) $(BOFS6:.bof=) $(BOFS7:.bof=) $(BOFS8:.bof=); do \ if [ -d $$i ]; \ then \ echo Building $$i; \ diff --git a/kod/makefile.linux b/kod/makefile.linux index ae0c8ba136..201f3a3cc0 100644 --- a/kod/makefile.linux +++ b/kod/makefile.linux @@ -1,72 +1,30 @@ # -# Makefile for compiling the Blakod. This is the 'main' makefile, so kod.mak -# is reproduced here with some extra commands to run after the makefile -# recurse is done (notably creating the client rsb file). We need to do this -# here instead of the root directory makefile in case anyone builds the blakod -# from this directory. +# Makefile for compiling the Blakod on Linux. +# Uses directory compile mode (-R) identical to Windows build +# to ensure consistent resource ID ordering. # TOPDIR=.. include $(TOPDIR)/common.mak.linux -BOFS = util.bof object.bof +BCFLAGS = -d -R -I $(KODINCLUDEDIR) -K $(KODDIR)/kodbase.txt -O $(BLAKSERVRUNDIR) -.SUFFIXES : .kod - -BCFLAGS = -d -I $(KODINCLUDEDIR) -K $(KODDIR)/kodbase.txt - -%.bof: %.kod - @$(BC) $(BCFLAGS) $< - $(CP) $@ $(BLAKSERVRUNDIR)/loadkod - if [ -f $(*F).rsc ]; \ - then \ - $(CP) $(*F).rsc $(BLAKSERVRUNDIR)/rsc; \ - fi - - -all : $(BOFS) $(BOFS2) $(BOFS3) $(BOFS4) $(BOFS5) $(BOFS6) $(BOFS7) $(BOFS8) - @for i in $(BOFS:.bof=) $(BOFS2:.bof=) $(BOFS3:.bof=.) $(BOFS4:.bof=.) $(BOFS5:.bof=.) $(BOFS6:.bof=.) $(BOFS7:.bof=.) $(BOFS8:.bof=.); do \ - echo Building $$i ; \ - cd $$i;\ - sed -e "s/\!include/include/" \ - -e "s/common.mak/common.mak.linux/" \ - -e "s/\\\\kod.mak/\/kod.mak.linux/" \ - -e "/include/ s:\\\\:\/:" \ - -e "/TOPDIR/ s:\\\\:\/:" \ - -e "/kod.mak/ s:\\\\:\/:" \ - -e "/DEPEND/ s:\\\\:\/:" \ - makefile >makefile.linux; \ - $(MAKE) -sf makefile.linux TOPDIR=../$(TOPDIR); \ - $(RM) makefile.linux; \ - cd ..; \ - done +all: + $(BC) $(BCFLAGS) $(KODDIR) @echo Copying kodbase.txt and kod include files -$(CP) $(KODDIR)/kodbase.txt $(BLAKSERVRUNDIR) 2>&1 - -@$(CP) $(KODDIR)/include/*.khd $(BLAKSERVRUNDIR) 2>&1 - @echo Copying .rsc and creating client .rsb file - @-$(RM) $(CLIENTRUNDIR)/resource/*.rsc 2>&1 - @-$(RM) $(CLIENTRUNDIR)/resource/*.rsb 2>&1 - -@$(RSCMERGE) $(CLIENTRUNDIR)/resource/rsc0000.rsb $(BLAKSERVRUNDIR)/rsc/*.rsc - -$(BOFS) $(BOFS2) $(BOFS3) $(BOFS4) $(BOFS5) $(BOFS6) $(BOFS7) $(BOFS8): $(DEPEND) - -clean : - @-$(RM) *.bof *.rsc kodbase.txt 2>&1 - @-for i in $(BOFS:.bof=); do \ - cd $$i; \ - sed -e "s/\!include/include/" \ - -e "s/common.mak/common.mak.linux/" \ - -e "s/\\\\kod.mak/\/kod.mak.linux/" \ - -e "/include/ s:\\\\:\/:" \ - -e "/TOPDIR/ s:\\\\:\/:" \ - -e "/kod.mak/ s:\\\\:\/:" \ - -e "/DEPEND/ s:\\\\:\/:" \ - makefile >makefile.linux; \ - $(MAKE) -sf makefile.linux TOPDIR=../$(TOPDIR) clean; \ - $(RM) makefile.linux; \ - cd ..; \ - done - $(RM) $(BLAKSERVRUNDIR)/rsc/*.rsc 2>&1 - $(RM) $(BLAKSERVRUNDIR)/loadkod/*.bof 2>&1 - $(RM) $(BLAKSERVRUNDIR)/memmap/*.bof 2>&1 - + -$(CP) $(KODDIR)/include/*.khd $(BLAKSERVRUNDIR) 2>&1 + @echo Creating server and client .rsb files + -$(RM) $(BLAKSERVRUNDIR)/rsc/*.rsb 2>/dev/null + -$(RM) $(CLIENTRUNDIR)/resource/*.rsb 2>/dev/null + $(RSCMERGE) $(BLAKSERVRUNDIR)/rsc/rsc0000.rsb $(BLAKSERVRUNDIR)/rsc + -$(CP) $(BLAKSERVRUNDIR)/rsc/rsc0000.rsb $(CLIENTRUNDIR)/resource/ 2>&1 + +clean: + -$(RM) $(BLAKSERVRUNDIR)/rsc/*.rsc 2>/dev/null + -$(RM) $(BLAKSERVRUNDIR)/rsc/*.rsb 2>/dev/null + -$(RM) $(BLAKSERVRUNDIR)/loadkod/*.bof 2>/dev/null + -$(RM) $(BLAKSERVRUNDIR)/memmap/*.bof 2>/dev/null + -$(RM) $(KODDIR)/kodbase.txt 2>/dev/null + -find $(KODDIR) -name "*.bof" -delete 2>/dev/null + -find $(KODDIR) -name "*.rsc" -delete 2>/dev/null diff --git a/kod/object/active/holder/nomoveon/battler/monster/npc/towns/razatwn/RazaVaultM.kod b/kod/object/active/holder/nomoveon/battler/monster/npc/towns/razatwn/razavaultm.kod similarity index 100% rename from kod/object/active/holder/nomoveon/battler/monster/npc/towns/razatwn/RazaVaultM.kod rename to kod/object/active/holder/nomoveon/battler/monster/npc/towns/razatwn/razavaultm.kod diff --git a/kod/object/active/holder/nomoveon/battler/monster/npc/wanderer/jGeneral.kod b/kod/object/active/holder/nomoveon/battler/monster/npc/wanderer/jgeneral.kod similarity index 100% rename from kod/object/active/holder/nomoveon/battler/monster/npc/wanderer/jGeneral.kod rename to kod/object/active/holder/nomoveon/battler/monster/npc/wanderer/jgeneral.kod diff --git a/kod/object/active/holder/room/bergrm/makefile b/kod/object/active/holder/room/bergrm/makefile index 98c278a03a..c025361c6c 100644 --- a/kod/object/active/holder/room/bergrm/makefile +++ b/kod/object/active/holder/room/bergrm/makefile @@ -7,7 +7,7 @@ DEPEND = ..\bergrm.bof BOFS = bergcity.bof bergvault.bof bergsmith.bof berghall.bof bergstore.bof \ - berginn.bof bergbank.bof bergauc.bof bergleader.bof bergleader_hall.bof \ + berginn.bof bergbank.bof bergauc.bof bergleader.bof bergleader_hall.bof \ bergjail.bof berg_devroom.bof !include $(KODDIR)\kod.mak diff --git a/kod/object/active/holder/room/dvalley5B.lkod b/kod/object/active/holder/room/dvalley5b.lkod similarity index 100% rename from kod/object/active/holder/room/dvalley5B.lkod rename to kod/object/active/holder/room/dvalley5b.lkod diff --git a/kod/object/active/holder/room/jixarm/jixa_smith.kod b/kod/object/active/holder/room/jixarm/jixa_smith.kod index a136a6a523..ccf45fe428 100644 --- a/kod/object/active/holder/room/jixarm/jixa_smith.kod +++ b/kod/object/active/holder/room/jixarm/jixa_smith.kod @@ -80,7 +80,7 @@ messages: Send(self,@NewHold,#what=Create(&DynamicLight,#iColor=LIGHT_FIRE,#iIntensity=45), #new_row=8,#new_col=8,#fine_row=12,#fine_col=44); - Send(self,@NewHold,#what=Create(&DecoItem,#name=hammer_name,#icon=hammer_icon,#desc=hammer_desc), + Send(self,@NewHold,#what=Create(&DecoItem,#name=hammer_jixa_name,#icon=hammer_jixa_icon,#desc=hammer_jixa_desc), #new_row=6,#new_col=9,#fine_row=46,#fine_col=56); propagate; diff --git a/kod/object/active/holder/room/razarm/razavault.lkod b/kod/object/active/holder/room/razarm/RazaVault.lkod similarity index 100% rename from kod/object/active/holder/room/razarm/razavault.lkod rename to kod/object/active/holder/room/razarm/RazaVault.lkod diff --git a/kod/object/item/passitem/defmod/helmet/makefile b/kod/object/item/passitem/defmod/helmet/makefile index b0efd720da..86a7f99ff6 100644 --- a/kod/object/item/passitem/defmod/helmet/makefile +++ b/kod/object/item/passitem/defmod/helmet/makefile @@ -5,7 +5,7 @@ !include $(TOPDIR)\common.mak DEPEND = ..\helmet.bof -BOFS = helm.bof circlet.bof ivycircl.bof simphelm.bof mask.bof qormashelm.bof \ +BOFS = helm.bof circlet.bof ivycircl.bof simphelm.bof mask.bof qormashelm.bof \ dhelm.bof thorncirc.bof warhelm.bof cowboyhat.bof !include $(KODDIR)\kod.mak diff --git a/kod/object/item/passitem/numbitem/food/makefile b/kod/object/item/passitem/numbitem/food/makefile index 0b5bb2631b..f4b22d7a06 100644 --- a/kod/object/item/passitem/numbitem/food/makefile +++ b/kod/object/item/passitem/numbitem/food/makefile @@ -11,7 +11,7 @@ BOFS = inkycap.bof snack.bof watrskin.bof meatpie.bof apple.bof \ gobletwn.bof kocmug.bof cheese.bof mint.bof turkyleg.bof chaosfood.bof \ cookie.bof pear.bof orange.bof telofruit.bof truffle.bof boonberry.bof \ kebab.bof fruitbowl.bof pancake.bof moonshine.bof pepper.bof sushi.bof \ - pretzel.bof corn.bgf pecanpie.bof pumppie.bof mashtaters.bof \ + pretzel.bof corn.bof pecanpie.bof pumppie.bof mashtaters.bof \ stuffing.bof ham.bof rouladen.bof !include $(KODDIR)\kod.mak diff --git a/kod/object/passive/spell/persench/signaturebuff/nimble.lkod b/kod/object/passive/spell/persench/signaturebuff/Nimble.lkod similarity index 100% rename from kod/object/passive/spell/persench/signaturebuff/nimble.lkod rename to kod/object/passive/spell/persench/signaturebuff/Nimble.lkod diff --git a/kod/object/passive/spell/persench/signaturebuff/tough.lkod b/kod/object/passive/spell/persench/signaturebuff/Tough.lkod similarity index 100% rename from kod/object/passive/spell/persench/signaturebuff/tough.lkod rename to kod/object/passive/spell/persench/signaturebuff/Tough.lkod diff --git a/kod/object/passive/trestype/makefile b/kod/object/passive/trestype/makefile index f58600d76b..ee06802e56 100644 --- a/kod/object/passive/trestype/makefile +++ b/kod/object/passive/trestype/makefile @@ -19,7 +19,7 @@ BOFS = wimptres.bof notres.bof spquent.bof med-tght.bof wmp-medt.bof \ xeoacidt.bof xeounholyt.bof xeoholyt.bof xeonerut.bof xeoillt.bof \ skel5t.bof necrotrs.bof minotres.bof chupatres.bof wolftres.bof \ vitkhult.bof lostsoultres.bof wendigot.bof spinebeastt.bof ranutres.bof \ - nyrphtres.bof easttres.bof swamptres.bof spinebeastbosst.kod nerutrollt.bof \ + nyrphtres.bof easttres.bof swamptres.bof spinebeastbosst.bof nerutrollt.bof \ mummylordt.bof yetit.bof snowmant.bof !include $(KODDIR)\kod.mak diff --git a/makefile.linux b/makefile.linux index 32aa9c7c93..b194ddc615 100644 --- a/makefile.linux +++ b/makefile.linux @@ -1,40 +1,44 @@ -# -# overall makefile -# - -TOPDIR=. -include common.mak.linux - -# make ignores targets if they match directory names -all: Bserver Bkod Butil - - -Bserver: - @echo Making $(COMMAND) in $(BLAKSERVDIR); \ - cd $(BLAKSERVDIR); \ - $(MAKE) -f makefile.linux $(COMMAND); \ - cd .. - -Bcompiler: - @echo Making $(COMMAND) in $(BLAKCOMPDIR); \ - cd $(BLAKCOMPDIR); \ - $(MAKE) -f makefile.linux $(COMMAND); \ - cd .. - -Bkod: Bcompiler Butil - @echo Making $(COMMAND) in $(KODDIR); \ - cd $(KODDIR); \ - $(MAKE) -sf makefile.linux $(COMMAND); \ - cd .. - -Butil: - @echo Making $(COMMAND) in $(UTILDIR); \ - cd $(UTILDIR); \ - $(MAKE) -f makefile.linux $(COMMAND); \ - cd .. - - -clean: - env COMMAND='clean' $(MAKE) -sf makefile.linux - $(RM) $(TOPDIR)/postbuild.log 2>&1 - $(RM) $(BLAKSERVDIR)/channel/*.txt +# +# overall makefile for Linux +# + +TOPDIR=. +include common.mak.linux + +# make ignores targets if they match directory names +all: Bserver Bresource Bkod Butil + +Bserver: + @echo Making $(COMMAND) in $(BLAKSERVDIR); \ + cd $(BLAKSERVDIR); \ + $(MAKE) -f makefile.linux $(COMMAND); \ + cd .. + +Bcompiler: + @echo Making $(COMMAND) in $(BLAKCOMPDIR); \ + cd $(BLAKCOMPDIR); \ + $(MAKE) -f makefile.linux $(COMMAND); \ + cd .. + +Bkod: Bcompiler Butil + @echo Making $(COMMAND) in $(KODDIR); \ + cd $(KODDIR); \ + $(MAKE) -f makefile.linux $(COMMAND); \ + cd .. + +Butil: + @echo Making $(COMMAND) in $(UTILDIR); \ + cd $(UTILDIR); \ + $(MAKE) -f makefile.linux $(COMMAND); \ + cd .. + +# Copy room files to server run directory (equivalent of Windows Bresource) +Bresource: + @echo Copying room files to server + @mkdir -p $(BLAKSERVRUNDIR)/rooms + -@$(CP) $(RESOURCEDIR)/rooms/*.roo $(BLAKSERVRUNDIR)/rooms/ 2>/dev/null + +clean: + env COMMAND='clean' $(MAKE) -sf makefile.linux + $(RM) $(TOPDIR)/postbuild.log 2>&1 + $(RM) $(BLAKSERVDIR)/channel/*.txt diff --git a/resource/rooms/razabank.roo b/resource/rooms/RazaBank.roo similarity index 100% rename from resource/rooms/razabank.roo rename to resource/rooms/RazaBank.roo diff --git a/resource/rooms/razavault.roo b/resource/rooms/RazaVault.roo similarity index 100% rename from resource/rooms/razavault.roo rename to resource/rooms/RazaVault.roo diff --git a/run/server/blakadmin.sh b/run/server/blakadmin.sh new file mode 100755 index 0000000000..9db0616d05 --- /dev/null +++ b/run/server/blakadmin.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# blakadmin.sh - Admin tool for Meridian 59 Linux server +# Sends commands to the maintenance port (default 9998) + +HOST="${BLAKSERV_HOST:-127.0.0.1}" +PORT="${BLAKSERV_PORT:-9998}" + +if [ $# -eq 0 ]; then + # Interactive mode + echo "Meridian 59 Admin Console ($HOST:$PORT)" + echo "Type commands, 'bye' to exit" + echo "---" + while true; do + read -p "blakadm> " cmd + [ -z "$cmd" ] && continue + [ "$cmd" = "bye" ] || [ "$cmd" = "quit" ] || [ "$cmd" = "exit" ] && break + python3 -c " +import socket, time +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.settimeout(5) +try: + s.connect(('$HOST', $PORT)) + s.sendall(b'$cmd\r\n') + time.sleep(0.3) + while True: + try: + data = s.recv(8192) + if not data: break + text = data.decode('latin-1') + for line in text.splitlines(): + if not line.startswith('> '): print(line) + except socket.timeout: + break +except Exception as e: + print(f'Error: {e}') +finally: + s.close() +" + done +else + # Single command mode + CMD="$*" + python3 -c " +import socket, time, sys +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.settimeout(5) +try: + s.connect(('$HOST', $PORT)) + s.sendall(b'$CMD\r\n') + time.sleep(0.3) + while True: + try: + data = s.recv(8192) + if not data: break + text = data.decode('latin-1') + for line in text.splitlines(): + if not line.startswith('> '): print(line) + except socket.timeout: + break +except Exception as e: + print(f'Error: {e}', file=sys.stderr) +finally: + s.close() +" +fi diff --git a/run/server/blakserv.cfg b/run/server/blakserv.cfg index a4b358030a..4f800610bb 100644 --- a/run/server/blakserv.cfg +++ b/run/server/blakserv.cfg @@ -2,7 +2,7 @@ # Configuration file automatically generated at Apr 11 2010 16:06:25 # ------------------------------------------- -[Path] +[Path] Bof loadkod\ Memmap memmap\ Rsc rsc\ @@ -14,48 +14,48 @@ Forms .\ Kodbase ..\..\kod PackageFile .\ -[Socket] +[Socket] -[Channel] +[Channel] DebugDisk Yes Flush Yes ErrorDisk Yes LogDisk Yes -[Account] +[Account] -[Login] +[Login] -[Inactive] +[Inactive] -[MessageOfTheDay] +[MessageOfTheDay] -[Session] +[Session] -[Lock] +[Lock] -[Resource] -Language 0 +[Resource] +Language 0 -[Memory] +[Memory] -[Auto] +[Auto] -[Update] +[Update] -[Console] +[Console] -[Rights] +[Rights] -[Constants] +[Constants] Enabled Yes Filename ..\..\kod\include\blakston.khd -[Advertise] +[Advertise] -[Debug] +[Debug] -[Security] +[Security] [Service] diff --git a/run/server/blakserv.cfg-linux b/run/server/blakserv.cfg-linux index 15992abc52..c007ff2777 100644 --- a/run/server/blakserv.cfg-linux +++ b/run/server/blakserv.cfg-linux @@ -1,8 +1,8 @@ -# BlakSton Server v2.1 (Feb 6 2010 19:36:48) -# Configuration file automatically generated at Apr 11 2010 16:06:25 +# BlakSton Server v2.1 - Linux Configuration +# Copy this file to blakserv.cfg before starting the server. # ------------------------------------------- -[Path] +[Path] Bof loadkod/ Memmap memmap/ Rsc rsc/ @@ -14,49 +14,50 @@ Forms ./ Kodbase ../../kod PackageFile ./ -[Socket] +[Socket] -[Channel] +[Channel] DebugDisk Yes +Flush Yes ErrorDisk Yes LogDisk Yes -[Account] +[Account] -[Login] +[Login] -[Inactive] +[Inactive] -[MessageOfTheDay] +[MessageOfTheDay] -[Session] +[Session] -[Lock] +[Lock] -[Resource] -Language 0 +[Resource] +Language 1 -[Memory] +[Memory] -[Auto] +[Auto] -[Update] +[Update] -[Console] +[Console] -[Rights] +[Rights] -[Constants] +[Constants] Enabled Yes Filename ../../kod/include/blakston.khd -[Advertise] +[Advertise] -[Debug] +[Debug] -[Security] +[Security] -[Service] +[Service] [MySQL] Enabled No diff --git a/run/server/rooms/.gitignore b/run/server/rooms/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/run/server/rooms/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/run/server/savegame/.gitignore b/run/server/savegame/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/run/server/savegame/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/util/makefile.linux b/util/makefile.linux index 4ecb773278..3223fad378 100644 --- a/util/makefile.linux +++ b/util/makefile.linux @@ -5,7 +5,7 @@ include $(TOPDIR)/common.mak.linux OBJS3 = $(OUTDIR)/scrshot.obj $(OUTDIR)/dibutil.obj $(OUTDIR)/copy.obj $(OUTDIR)/file.obj -CFLAGS = +CFLAGS = -DBLAK_PLATFORM_LINUX -Wno-write-strings SOURCEDIR = $(TOPDIR)/util diff --git a/util/rscmerge.c b/util/rscmerge.c index 9fbce91a5b..bc3ef80fd7 100644 --- a/util/rscmerge.c +++ b/util/rscmerge.c @@ -6,10 +6,23 @@ #include #include #include + +#ifdef BLAK_PLATFORM_WINDOWS #include +#endif + +#ifdef BLAK_PLATFORM_LINUX +#include +#include +#ifndef MAX_PATH +#define MAX_PATH PATH_MAX +#endif +#endif + #include "rscload.h" #include #include +#include typedef std::vector StringVector; @@ -80,9 +93,10 @@ bool FindMatchingFiles(char *path, std::vector *files) } while (FindNextFile(hFindFile, &search_data)); FindClose(hFindFile); + std::sort(files->begin(), files->end()); return true; -#elif BLAK_PLATFORM_LINUX +#elif defined(BLAK_PLATFORM_LINUX) // Warning, not tested in rscmerge.c. struct dirent *entry; std::string spath = path; @@ -109,6 +123,10 @@ bool FindMatchingFiles(char *path, std::vector *files) closedir(dir); + std::sort(files->begin(), files->end(), + [](const std::string &a, const std::string &b) { + return strcasecmp(a.c_str(), b.c_str()) < 0; + }); return true; #else @@ -241,13 +259,32 @@ bool EachRscCallback(char *filename, int rsc, int lang_id, char *string) bool LoadRscFiles(int num_files, char **foldername) { char file_load_path[MAX_PATH + FILENAME_MAX]; +#ifdef BLAK_PLATFORM_LINUX + sprintf(file_load_path, "%s/*.rsc", *foldername); +#else sprintf(file_load_path, "%s\\*.rsc", *foldername); +#endif StringVector files; if (FindMatchingFiles(file_load_path, &files)) { for (StringVector::iterator it = files.begin(); it != files.end(); ++it) { +#ifdef BLAK_PLATFORM_LINUX + sprintf(file_load_path, "%s/%s", *foldername, it->c_str()); +#else sprintf(file_load_path, "%s\\%s", *foldername, it->c_str()); +#endif + + // Skip empty .rsc files (classes with no resources) + FILE *fcheck = fopen(file_load_path, "rb"); + if (fcheck) + { + fseek(fcheck, 0, SEEK_END); + long fsize = ftell(fcheck); + fclose(fcheck); + if (fsize == 0) + continue; + } if (!RscFileLoad(file_load_path, EachRscCallback)) {