From 557cbc6b6b89eecb7eb42cf537ae65a29af41dfa Mon Sep 17 00:00:00 2001 From: Arantis Date: Tue, 24 Mar 2026 21:14:07 +0100 Subject: [PATCH 01/11] Linux server port - fully functional Server: - Single-threaded epoll architecture (based on vanilla Meridian59) - osd_linux.c/h, osd_epoll.c: OS abstraction layer - main.c: Unified Windows/Linux with #ifdef - Admin commands via maintenance port (TCP 9998) - Shutdown command + SIGTERM signal handler - PostgreSQL database support (replaces MySQL on Linux) - database_pg.c/h: Full implementation with 46 tables - Async writer thread with queue - Docker compose for PostgreSQL + pgAdmin - A* pathfinding parallelized with OpenMP - UDP protocol support via epoll Build system: - KOD compilation with directory mode (bc -R) for identical resource IDs between Windows and Linux builds - All makefiles updated for Linux (blakserv, blakcomp, util, kod) - blakcomp: dircompile.c ported for Linux path handling Bug fixes (both platforms): - Fixed trailing whitespace in helmet/bergrm makefiles - Fixed corn.bgf -> corn.bof typo in food makefile - Fixed spinebeastbosst.kod -> .bof in trestype makefile - Fixed jixa_smith.kod wrong resource variable names - Fixed case-sensitive file renames for Linux filesystem --- LINUX_BUILD.md | 117 +++ blakcomp/blakcomp.h | 4 + blakcomp/blakcomp.l | 11 +- blakcomp/codegen.c | 12 +- blakcomp/dircompile.c | 45 +- blakcomp/makefile.linux | 1 + blakserv/adminfn.c | 20 +- blakserv/astar.c | 10 + blakserv/async.c | 46 + blakserv/async.h | 4 + blakserv/blakserv.h | 22 +- blakserv/ccode.c | 10 +- blakserv/database_pg.c | 940 ++++++++++++++++++ blakserv/database_pg.h | 111 +++ blakserv/game.c | 2 - blakserv/intrlock.c | 8 +- blakserv/main.c | 178 +++- blakserv/makefile.linux | 29 +- blakserv/osd_epoll.c | 301 ++++++ blakserv/osd_linux.c | 211 ++++ blakserv/osd_linux.h | 135 +++ blakserv/synched.c | 10 + blakserv/timer.c | 16 + blakserv/timer.h | 2 + docker/postgres/docker-compose.yml | 28 + kod/kod.mak.linux | 2 +- kod/makefile.linux | 81 +- .../{RazaVaultM.kod => razavaultm.kod} | 0 .../wanderer/{jGeneral.kod => jgeneral.kod} | 0 kod/object/active/holder/room/bergrm/makefile | 2 +- .../room/{dvalley5B.lkod => dvalley5b.lkod} | 0 .../active/holder/room/jixarm/jixa_smith.kod | 2 +- .../razarm/{razavault.lkod => RazaVault.lkod} | 0 .../item/passitem/defmod/helmet/makefile | 2 +- .../item/passitem/numbitem/food/makefile | 2 +- .../{nimble.lkod => Nimble.lkod} | 0 .../signaturebuff/{tough.lkod => Tough.lkod} | 0 kod/object/passive/trestype/makefile | 2 +- makefile.linux | 84 +- resource/rooms/{razabank.roo => RazaBank.roo} | Bin .../rooms/{razavault.roo => RazaVault.roo} | Bin rsc0000.rsb | Bin 0 -> 2407917 bytes run/server/blakserv.cfg | 84 +- util/makefile.linux | 2 +- util/rscmerge.c | 22 +- 45 files changed, 2336 insertions(+), 222 deletions(-) create mode 100644 LINUX_BUILD.md create mode 100644 blakserv/database_pg.c create mode 100644 blakserv/database_pg.h create mode 100644 blakserv/osd_epoll.c create mode 100644 blakserv/osd_linux.c create mode 100644 blakserv/osd_linux.h create mode 100644 docker/postgres/docker-compose.yml rename kod/object/active/holder/nomoveon/battler/monster/npc/towns/razatwn/{RazaVaultM.kod => razavaultm.kod} (100%) rename kod/object/active/holder/nomoveon/battler/monster/npc/wanderer/{jGeneral.kod => jgeneral.kod} (100%) rename kod/object/active/holder/room/{dvalley5B.lkod => dvalley5b.lkod} (100%) rename kod/object/active/holder/room/razarm/{razavault.lkod => RazaVault.lkod} (100%) rename kod/object/passive/spell/persench/signaturebuff/{nimble.lkod => Nimble.lkod} (100%) rename kod/object/passive/spell/persench/signaturebuff/{tough.lkod => Tough.lkod} (100%) rename resource/rooms/{razabank.roo => RazaBank.roo} (100%) rename resource/rooms/{razavault.roo => RazaVault.roo} (100%) create mode 100644 rsc0000.rsb diff --git a/LINUX_BUILD.md b/LINUX_BUILD.md new file mode 100644 index 0000000000..29ef5c6d6d --- /dev/null +++ b/LINUX_BUILD.md @@ -0,0 +1,117 @@ +# Meridian 59 Server - Linux Build Instructions + +## Prerequisites (Ubuntu/Debian) + +```bash +sudo dpkg --add-architecture i386 +sudo apt-get update +sudo apt-get install gcc-multilib g++-multilib flex bison \ + libjansson-dev libjansson-dev:i386 \ + libpq-dev libpq-dev:i386 \ + ncat python3 +``` + +## Build + +```bash +make -f makefile.linux +``` + +Builds: blakserv, blakcomp, rscmerge, all KOD/BOF/RSC/RSB files, copies rooms. + +## Configuration + +```bash +cd run/server +cp blakserv.cfg-linux blakserv.cfg +mkdir -p savegame +``` + +Edit `blakserv.cfg` - important settings: +```ini +[Path] +Rooms rooms/ + +[Socket] +MaintenanceMask ::ffff:127.0.0.1;::1 + +[Channel] +Flush Yes + +[Memory] +SizeClassHash 199999 + +[Resource] +RscSpec *.rsb + +[MySQL] +Enabled Yes +Host 127.0.0.1 +Port 5432 +Username blakserv +Password blaks3kr1t +Database meridian59 +``` + +Set `[MySQL] Enabled No` to run without database. + +## PostgreSQL Setup + +```bash +cd docker/postgres +docker compose up -d +``` + +This starts a PostgreSQL 16 container on port 5432. The server creates all ~46 tables automatically on first connect. + +Test with: +```bash +docker exec m59-postgres psql -U blakserv -d meridian59 -c "\dt" +``` + +## Run + +```bash +cd run/server +./blakserv & # background +./blakserv # foreground (Ctrl+C to stop) +``` + +Ports: **5959** (game), **9998** (admin maintenance) + +## Admin Commands + +```bash +bash blakadmin.sh show status +bash blakadmin.sh show accounts +bash blakadmin.sh who +bash blakadmin.sh save game +bash blakadmin.sh send o 0 updatedatabase +bash blakadmin.sh send o 0 getuniqueips +bash blakadmin.sh create account admin username password email +bash blakadmin.sh shutdown # save + stop server + +# Interactive: +bash blakadmin.sh +blakadm> show status +blakadm> bye +``` + +## Logs + +```bash +tail -f run/server/channel/*.txt +``` + +Files: `debug.txt`, `error.txt`, `log.txt`, `god.txt`, `admin.txt` + +## Architecture + +Single-threaded epoll main loop (based on vanilla Meridian59 repo), with a separate +PostgreSQL writer thread for async database operations. + +- `osd_linux.c/h` - OS-dependent types and stubs +- `osd_epoll.c` - Main loop (epoll socket multiplexing + timers) +- `database_pg.c/h` - PostgreSQL database layer (replaces MySQL) +- `main.c` - Unified Windows/Linux main with `#ifdef` +- Admin via maintenance port only (no console `-i` mode) 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..eee47cd478 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; @@ -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..9e8c1315c0 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) { @@ -260,7 +285,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,7 +301,7 @@ 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; @@ -323,7 +348,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 +388,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/makefile.linux b/blakcomp/makefile.linux index 0fed6b61b2..6962f24132 100644 --- a/blakcomp/makefile.linux +++ b/blakcomp/makefile.linux @@ -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/adminfn.c b/blakserv/adminfn.c index 6558e6917a..99d37c4316 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 } @@ -5601,9 +5604,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..40a8ac9188 100644 --- a/blakserv/async.c +++ b/blakserv/async.c @@ -359,7 +359,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 +426,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 +512,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..66a5ef465f 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" @@ -279,9 +283,8 @@ char * GetLastErrorStr(); #ifdef BLAK_PLATFORM_WINDOWS #include "interface_windows.h" -#else -#include "interface_linux.h" #endif +// Linux interface functions declared in osd_linux.h #include "intrlock.h" #include "chanbuf.h" @@ -297,8 +300,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 +322,8 @@ char * GetLastErrorStr(); #ifdef BLAK_PLATFORM_WINDOWS #include "database.h" +#else +#include "database_pg.h" #endif #include "jansson.h" 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/database_pg.c b/blakserv/database_pg.c new file mode 100644 index 0000000000..b7710b455b --- /dev/null +++ b/blakserv/database_pg.c @@ -0,0 +1,940 @@ +// 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 +static sql_statistic_type Statistics_Table[] = { + {STAT_NONE, 0, false, NULL}, + {STAT_TOTALMONEY, 1, true, "money_total"}, + {STAT_MONEYCREATED, 1, true, "money_created"}, + {STAT_PLAYERLOGIN, 3, true, "player_logins"}, + {STAT_ASSESS_DAM, 7, true, "player_damaged"}, + {STAT_PLAYERDEATH, 5, true, "player_death"}, + {STAT_PLAYER, 13, true, "player"}, + {STAT_PLAYERSUICIDE, 2, true, "player"}, + {STAT_GUILD, 4, true, "guild"}, + {STAT_GUILDDISBAND, 1, true, "guild"}, + {STAT_SPELLS, 14, true, "wiki_spells"}, + {STAT_SPELL_REAGENT, 3, true, "wiki_spell_reagent"}, + {STAT_TREASURE_GEN, 3, true, "wiki_treasure_gen"}, + {STAT_MONSTER, 13, true, "wiki_monster"}, + {STAT_MONSTER_ZONE, 3, true, "wiki_monster_zone"}, + {STAT_NPCS, 7, true, "wiki_npcs"}, + {STAT_WEAPONS, 14, true, "wiki_weapons"}, + {STAT_ROOMS, 6, true, "wiki_rooms"}, + {STAT_NPC_ZONE, 4, true, "wiki_npc_zone"}, + {STAT_NPC_SELLITEM, 3, true, "wiki_npc_sellitem"}, + {STAT_NPC_SELLSKILL, 2, true, "wiki_npc_sellskill"}, + {STAT_NPC_SELLSPELL, 2, true, "wiki_npc_sellspell"}, + {STAT_REAGENTS, 11, true, "wiki_reagents"}, + {STAT_FOOD, 11, true, "wiki_food"}, + {STAT_AMMO, 11, true, "wiki_ammo"}, + {STAT_ARMOR, 11, true, "wiki_armor"}, + {STAT_MISCITEMS, 11, true, "wiki_miscitems"}, + {STAT_RINGS, 11, true, "wiki_rings"}, + {STAT_RODS, 11, true, "wiki_rods"}, + {STAT_POTIONS, 11, true, "wiki_potions"}, + {STAT_SCROLLS, 11, true, "wiki_scrolls"}, + {STAT_WANDS, 11, true, "wiki_wands"}, + {STAT_QUESTITEMS, 11, true, "wiki_questitems"}, + {STAT_SKILLS, 12, true, "wiki_skills"}, + {STAT_NECKLACE, 11, true, "wiki_necklace"}, + {STAT_INSTRUMENTS, 11, true, "wiki_instruments"}, + {STAT_GEMS, 11, true, "wiki_gems"}, + {STAT_OFFERINGS, 11, true, "wiki_offerings"}, + {STAT_QUESTS, 11, true, "wiki_quests"}, + {STAT_TREASURE_EXTRA, 4, true, "wiki_treasure_extra"}, + {STAT_TREASURE_MAGIC, 3, true, "wiki_treasure_magic"}, + {STAT_NPC_SELLCOND, 4, true, "wiki_npc_sellcond"}, + {STAT_LOGPEN, 7, true, "player_logpen"}, + {STAT_LOGPEN_ITEM, 3, true, "player_logpen_items"}, + {STAT_QUESTGIVER, 2, true, "wiki_quest_giver"}, + {STAT_ACCOUNTS, 8, true, "server_accounts"}, + {STAT_ACCOUNT_CHARS, 2, true, "server_account_chars"}, + {STAT_COMPL_QUESTS, 5, true, "player_completed_quests"}, + {STAT_COMPL_QUEST_ITEMS,4, true, "player_quest_reward_items"} +}; + +/******************************************************************************/ +// 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 + char sql[4096]; + char placeholders[1024]; + placeholders[0] = 0; + for (int i = 0; i < nParams; i++) + { + char ph[8]; + snprintf(ph, sizeof(ph), "%s$%d", i > 0 ? "," : "", i + 1); + strcat(placeholders, ph); + } + 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..4456f7441e --- /dev/null +++ b/blakserv/database_pg.h @@ -0,0 +1,111 @@ +// 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; +}; + +typedef struct { + sql_recordtype stat_type; + int num_fields; + bool enabled; + const char *table_name; +} 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/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/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/main.c b/blakserv/main.c index dea8509c62..9e631bb0ee 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,155 @@ 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 int main(int argc, char **argv) { - main_thread_id = pthread_self(); + MainServer(); + return 0; +} - return MainServer(argc,argv); +#endif + +#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(); + InitInterface(); + + 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(); + InterfaceUpdate(); + + lprintf("Status: %i accounts\n", GetNextAccountID()); + lprintf("-----------------------------------------------------------------------------------------------\n"); + dprintf("-----------------------------------------------------------------------------------------------\n"); + eprintf("-----------------------------------------------------------------------------------------------\n"); + gprintf("-----------------------------------------------------------------------------------------------\n"); + + in_main_loop = true; + + AsyncSocketStart(); + +#ifdef BLAK_PLATFORM_WINDOWS + SetWindowText(hwndMain, ConfigStr(CONSOLE_CAPTION)); + ServiceTimers(); +#else + RunMainLoop(); #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() + MainExitServer(); +} + +void MainExitServer(void) +{ + lprintf("ExitServer terminating server\n"); + + ExitAsyncConnections(); + 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 +199,6 @@ void MainReloadGameData() ResetMessage(); ResetClass(); - // Reload data. InitNameID(); LoadMotd(); LoadBof(); @@ -85,22 +209,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..da9f67aee6 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 @@ -17,20 +17,18 @@ 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 = -m32 -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 = -m32 -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 +47,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 +94,10 @@ OBJS = \ $(OUTDIR)/intstringhash.obj \ $(OUTDIR)/files.obj \ $(OUTDIR)/sprocket.obj \ - $(OUTDIR)/linux-critical_section.obj \ - $(OUTDIR)/interface_linux.obj \ - $(OUTDIR)/mutex_linux.obj \ - $(OUTDIR)/thdmsgqueue_linux.obj \ - $(OUTDIR)/astar.obj \ + $(OUTDIR)/astar.obj \ + $(OUTDIR)/osd_linux.obj \ + $(OUTDIR)/osd_epoll.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..96e16b6205 --- /dev/null +++ b/blakserv/osd_epoll.c @@ -0,0 +1,301 @@ +// 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 + +int fd_epoll; + +#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; + + // 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 + { + AsyncSocketAccept(notify_events[i].data.fd, FD_ACCEPT, 0, + GetAcceptingSocketConnectionType(notify_events[i].data.fd)); + } + } + 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); +} + +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 | EPOLLOUT | EPOLLET; + 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 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; + + 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..d34c680b07 --- /dev/null +++ b/blakserv/osd_linux.c @@ -0,0 +1,211 @@ +// 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 + +static int sessions_logged_on = 0; + +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); +} + +void InitInterface(void) +{ + // No console interface on Linux - admin via maintenance port +} + +int GetUsedSessions(void) +{ + return sessions_logged_on; +} + +void StartupPrintf(const char *fmt, ...) +{ + char s[200]; + va_list marker; + + va_start(marker, fmt); + vsnprintf(s, sizeof(s), fmt, marker); + va_end(marker); + + if (strlen(s) > 0) + { + if (s[strlen(s)-1] == '\n') + s[strlen(s)-1] = 0; + } + + printf("Startup: %s\n", s); +} + +void InterfaceUpdate(void) +{ +} + +void InterfaceLogon(session_node *s) +{ + sessions_logged_on++; +} + +void InterfaceLogoff(session_node *s) +{ + sessions_logged_on--; +} + +void InterfaceUpdateSession(session_node *s) +{ +} + +void InterfaceUpdateChannel(void) +{ +} + +void InterfaceSendBufferList(buffer_node *blist) +{ + // No console admin on Linux - admin responses go via maintenance port + DeleteBufferList(blist); +} + +void InterfaceSendBytes(char *buf, int len_buf) +{ + // No console admin on Linux +} + +HANDLE StartAsyncNameLookup(char *peer_addr, char *buf) +{ + // DNS reverse lookup not implemented on Linux + return 0; +} + +void FatalErrorShow(const char *filename, int line, const char *str) +{ + fprintf(stderr, "FATAL ERROR: File %s line %i\n%s\n", filename, line, str); + exit(1); +} + +// 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..10d047fc32 --- /dev/null +++ b/blakserv/osd_linux.h @@ -0,0 +1,135 @@ +// 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 + +#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 unsigned long 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 unsigned long 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 InitInterface(void); +int GetUsedSessions(void); + +void StartupPrintf(const char *fmt,...); +void StartupComplete(void); + +void InterfaceUpdate(void); +void InterfaceUpdateChannel(void); + +void InterfaceSendBytes(char *buf,int len_buf); + +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/synched.c b/blakserv/synched.c index f47982a381..d5e647d1a3 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,10 @@ void VerifyLogin(session_node *s) SetSessionTimer(s, 60 * ConfigInt(INACTIVE_TRANSFER)); } else + { + dprintf("RSB OK - calling SynchedDoMenu\n"); SynchedDoMenu(s); + } } } diff --git a/blakserv/timer.c b/blakserv/timer.c index a4136a41d3..01d591a042 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,10 @@ __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); +#endif + // On Linux, RunMainLoop recalculates wait time each iteration } // Start node off at end of heap. @@ -364,6 +371,7 @@ void TimerActivate() } } +#ifdef BLAK_PLATFORM_WINDOWS Bool InMainLoop(void) { return in_main_loop; @@ -467,6 +475,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/docker/postgres/docker-compose.yml b/docker/postgres/docker-compose.yml new file mode 100644 index 0000000000..6d1b26e29e --- /dev/null +++ b/docker/postgres/docker-compose.yml @@ -0,0 +1,28 @@ +services: + postgres: + image: postgres:16 + container_name: m59-postgres + restart: unless-stopped + environment: + POSTGRES_USER: blakserv + POSTGRES_PASSWORD: blaks3kr1t + POSTGRES_DB: meridian59 + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + pgadmin: + image: dpage/pgadmin4 + container_name: m59-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@meridian59.de + PGADMIN_DEFAULT_PASSWORD: admin + ports: + - "5050:80" + depends_on: + - postgres + +volumes: + pgdata: 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..b29d613551 100644 --- a/kod/makefile.linux +++ b/kod/makefile.linux @@ -1,72 +1,27 @@ # -# 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) $(KODDIR)/kodbase.txt 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/rsc0000.rsb b/rsc0000.rsb new file mode 100644 index 0000000000000000000000000000000000000000..f439942b4d339df8d9bbcd3542ffd82e5ba76a71 GIT binary patch literal 2407917 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