diff --git a/CMake/Dependencies.cmake b/CMake/Dependencies.cmake index d993553667f..bc3e180ffee 100644 --- a/CMake/Dependencies.cmake +++ b/CMake/Dependencies.cmake @@ -2,6 +2,7 @@ # and whether to link them statically. include(functions/dependency_options) include(functions/emscripten_system_library) +include(CheckCXXSourceCompiles) if(EMSCRIPTEN) emscripten_system_library("zlib" ZLIB::ZLIB USE_ZLIB=1) @@ -275,3 +276,19 @@ if(GPERF) find_package(Gperftools REQUIRED) message("INFO: ${GPERFTOOLS_LIBRARIES}") endif() + +check_cxx_source_compiles(" + extern char __bss_start; + extern char _end; + #include + int main() { + assert(&__bss_start < &_end); + return 0; + } +" HAVE_LINKER_BSS_SYMBOLS) + +if(HAVE_LINKER_BSS_SYMBOLS) + message(STATUS "Linker symbols `__bss_start` and `_end` are available") +else() + message(STATUS "Linker symbols `__bss_start` and `_end` are NOT available") +endif() diff --git a/CMake/functions/devilutionx_library.cmake b/CMake/functions/devilutionx_library.cmake index f40fa6a0910..575137fcdb1 100644 --- a/CMake/functions/devilutionx_library.cmake +++ b/CMake/functions/devilutionx_library.cmake @@ -67,6 +67,10 @@ function(add_devilutionx_library NAME) target_compile_definitions(${NAME} PUBLIC ${DEVILUTIONX_DEFINITIONS}) set_relative_file_macro(${NAME}) + + if(HAVE_LINKER_BSS_SYMBOLS) + target_compile_definitions(${NAME} PUBLIC HAVE_LINKER_BSS_SYMBOLS) + endif() endfunction() # Same as add_devilutionx_library(${NAME} OBJECT). diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 0acdb6931e0..42c13c0b114 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -165,7 +165,8 @@ set(libdevilutionx_SRCS utils/sdl_bilinear_scale.cpp utils/sdl_thread.cpp utils/surface_to_clx.cpp - utils/timer.cpp) + utils/timer.cpp + utils/mapping.cpp) # These files are responsible for most of the runtime in Debug mode. # Apply some optimizations to them even in Debug mode to get reasonable performance. diff --git a/Source/DiabloUI/settingsmenu.cpp b/Source/DiabloUI/settingsmenu.cpp index ef1c33f2805..0386e673c28 100644 --- a/Source/DiabloUI/settingsmenu.cpp +++ b/Source/DiabloUI/settingsmenu.cpp @@ -205,6 +205,7 @@ bool ChangeOptionValue(OptionEntryBase *pOption, size_t listIndex) auto *pOptionList = static_cast(pOption); pOptionList->SetActiveListIndex(listIndex); } break; + case OptionEntryType::String: case OptionEntryType::Key: case OptionEntryType::PadButton: break; diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 9b09f40a2f1..5ee31aa2aaa 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -286,7 +286,12 @@ void FindRangedTarget() void FindMeleeTarget() { bool visited[MAXDUNX][MAXDUNY] = { {} }; - int maxSteps = 25; // Max steps for FindPath is 25 + // Max steps for FindPath is 25 + int maxSteps = 25; + if (*GetOptions().Gameplay.noMonstersAutoPursuing) + // Disable monster auto-pursuing + maxSteps = 0; + int rotations = 0; bool canTalk = false; diff --git a/Source/controls/touch/renderers.cpp b/Source/controls/touch/renderers.cpp index 0352f27a91e..fac23868219 100644 --- a/Source/controls/touch/renderers.cpp +++ b/Source/controls/touch/renderers.cpp @@ -235,7 +235,7 @@ void VirtualDirectionPadRenderer::LoadArt() void VirtualGamepadRenderer::Render(RenderFunction renderFunction) { - if (CurrentEventHandler == DisableInputEventHandler) + if (CurrentEventHandler.handle == DisableInputEventHandler) return; primaryActionButtonRenderer.Render(renderFunction, buttonArt); diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 2fab97a4edf..f6f0182ef94 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -101,6 +101,7 @@ #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" +#include "utils/shared.h" #ifndef USE_SDL1 #include "controls/touch/gamepad.h" @@ -486,6 +487,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) return; if (gmenu_presskeys(vkey) || CheckKeypress(vkey)) { + printf(">> %s:%d, not handled\n", __func__, __LINE__); return; } @@ -841,12 +843,15 @@ void RunGameLoop(interface_mode uMsg) nthread_ignore_mutex(true); StartGame(uMsg); assert(HeadlessMode || ghMainWnd); - EventHandler previousHandler = SetEventHandler(GameEventHandler); + EventHandler newHandler = { GameEventHandler, SDL_PollEvent }; + EventHandler previousHandler = SetEventHandler(newHandler); run_delta_info(); gbRunGame = true; gbProcessPlayers = IsDiabloAlive(true); gbRunGameResult = true; + printf(">> %s\n", __func__); + RedrawEverything(); if (!HeadlessMode) { while (IsRedrawEverything()) { @@ -869,6 +874,11 @@ void RunGameLoop(interface_mode uMsg) unsigned run_game_iteration = 0; #endif + /* Start a new level on new game start if specified */ + if (uMsg == WM_DIABNEWGAME && *GetOptions().Gameplay.gameLevel) + StartNewLvl(*MyPlayer, interface_mode::WM_DIABNEXTLVL, + *GetOptions().Gameplay.gameLevel); + while (gbRunGame) { #ifdef _DEBUG @@ -937,7 +947,7 @@ void RunGameLoop(interface_mode uMsg) RedrawEverything(); scrollrt_draw_game_screen(); previousHandler = SetEventHandler(previousHandler); - assert(HeadlessMode || previousHandler == GameEventHandler); + assert(HeadlessMode || previousHandler.handle == GameEventHandler); FreeGame(); if (cineflag) { @@ -1179,8 +1189,12 @@ void ApplicationInit() if (*GetOptions().Graphics.showFPS) EnableFrameCount(); - init_create_window(); - was_window_init = true; + if (!HeadlessMode) { + init_create_window(); + was_window_init = true; + } else { + SDL_Init(SDL_INIT_EVENTS); + } InitializeScreenReader(); LanguageInitialize(); @@ -1236,9 +1250,10 @@ void DiabloInit() DiabloInitScreen(); - snd_init(); - - ui_sound_init(); + if (!HeadlessMode) { + snd_init(); + ui_sound_init(); + } // Item graphics are loaded early, they already get touched during hero selection. InitItemGFX(); @@ -2524,6 +2539,9 @@ bool StartGame(bool bNewGame, bool bSinglePlayer) gbSelectProvider = true; ReturnToMainMenu = false; + printf(">> %s: newgame=%d, single=%d\n", __func__, + bNewGame, bSinglePlayer); + do { gbLoadGame = false; @@ -2585,6 +2603,8 @@ void setOnInitialized(void (*callback)()) int DiabloMain(int argc, char **argv) { + setlinebuf(stdout); + #ifdef _DEBUG SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); #endif @@ -2608,6 +2628,12 @@ int DiabloMain(int argc, char **argv) LuaInitialize(); if (!demo::IsRunning()) SaveOptions(); + if (!(*GetOptions().Gameplay.shareGameStateFilename).empty()) { + // Share whole diablo state + shared::share_diablo_state(paths::ConfigPath() + "/" + + *GetOptions().Gameplay.shareGameStateFilename); + } + // Finally load game data LoadGameArchives(); @@ -2740,8 +2766,10 @@ void diablo_pause_game() if (!gbIsMultiplayer) { if (PauseMode != 0) { PauseMode = 0; + printf(">> %s: resumed\n", __func__); } else { PauseMode = 2; + printf(">> %s: paused\n", __func__); sound_stop(); qtextflag = false; LastMouseButtonAction = MouseActionType::None; @@ -3300,6 +3328,133 @@ tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir) return {}; } +static void +inject_sdl_events(uint32_t new_type) +{ + static uint32_t prev_type; + + unsigned int sdl_type, sdl_sym; + SDL_Scancode sdl_scan; + unsigned int diff, i; + SDL_Event event; + + diff = prev_type ^ new_type; + prev_type = new_type; + + for (i = 0; i < sizeof(diff) * 8; i++) { + unsigned int bit = (1 << i); + if (!(diff & bit)) + continue; + + sdl_type = (new_type & bit ? SDL_KEYDOWN : SDL_KEYUP); + + if (bit == RING_ENTRY_KEY_LEFT) { + sdl_sym = SDLK_LEFT; + sdl_scan = SDL_SCANCODE_LEFT; + } else if (bit == RING_ENTRY_KEY_RIGHT) { + sdl_sym = SDLK_RIGHT; + sdl_scan = SDL_SCANCODE_RIGHT; + } else if (bit == RING_ENTRY_KEY_UP) { + sdl_sym = SDLK_UP; + sdl_scan = SDL_SCANCODE_UP; + } else if (bit == RING_ENTRY_KEY_DOWN) { + sdl_sym = SDLK_DOWN; + sdl_scan = SDL_SCANCODE_DOWN; + } else if (bit == RING_ENTRY_KEY_X) { + sdl_sym = SDLK_x; + sdl_scan = SDL_SCANCODE_X; + } else if (bit == RING_ENTRY_KEY_Y) { + sdl_sym = SDLK_y; + sdl_scan = SDL_SCANCODE_Y; + } else if (bit == RING_ENTRY_KEY_A) { + sdl_sym = SDLK_a; + sdl_scan = SDL_SCANCODE_A; + } else if (bit == RING_ENTRY_KEY_B) { + sdl_sym = SDLK_b; + sdl_scan = SDL_SCANCODE_B; + } else if (bit == RING_ENTRY_KEY_SAVE) { + sdl_sym = SDLK_F2; + sdl_scan = SDL_SCANCODE_F2; + + if (sdl_type == SDL_KEYDOWN) + printf(">> %s: received SAVE\n", __func__); + } else if (bit == RING_ENTRY_KEY_LOAD) { + sdl_sym = SDLK_F3; + sdl_scan = SDL_SCANCODE_F3; + + if (sdl_type == SDL_KEYDOWN) + printf(">> %s: received LOAD\n", __func__); + } else if (bit == RING_ENTRY_KEY_PAUSE) { + sdl_sym = SDLK_PAUSE; + sdl_scan = SDL_SCANCODE_PAUSE; + + if (sdl_type == SDL_KEYDOWN) + printf(">> %s: received PAUSE\n", __func__); + } else { + /* Unknown key */ + continue; + } + + event.type = sdl_type; + event.key.keysym.sym = sdl_sym; + event.key.keysym.scancode = sdl_scan; + event.key.keysym.mod = KMOD_NONE; + event.key.repeat = 0; + + if (SDL_PushEvent(&event) < 0) { + printf("Failed to push event: %s\n", SDL_GetError()); + } + } +} + +static void update_shared_state(void) +{ + static bool release_keys; + struct ring_entry *entry; + + if (MyPlayer) + // Do a static cast to avoid compiler warning that writing to + // an object with no trivial copy-assignment is not + // allowed. Please, FIXME. + memcpy(static_cast(&shared::player), MyPlayer, sizeof(shared::player)); + + // "Odd" phase of a tick before key injection. Two phases are + // needed for proper synchronization from the submitter's side, + // ensuring that the submitter knows exactly one full game tick + // has passed since the last key acceptance. + shared::game_ticks++; + // Prevent compiler from moving increment below the barrier + std::atomic_signal_fence(std::memory_order_seq_cst); + + // On early start, the game state is not completely initialized, + // so incoming keys may not work. Delay keys processing. The + // number of ticks to delay was chosen empirically. Also ticks are + // multiplied by two, since there are two phases. + if (shared::game_ticks > 5*2) { + entry = ring_queue_get_entry_to_retreive(&shared::input_queue); + if (entry) { + uint32_t keys = entry->type; + + release_keys = (keys & RING_ENTRY_F_SINGLE_TICK_PRESS); + keys &= ~RING_ENTRY_FLAGS; + inject_sdl_events(keys); + ring_queue_retrieve(&shared::input_queue); + + /* Reply with key event */ + entry = ring_queue_get_entry_to_submit(&shared::events_queue); + entry->type = keys; + ring_queue_submit(&shared::events_queue); + } else if (release_keys) { + release_keys = false; + inject_sdl_events(0); + } + } + // Prevent compiler from moving instructions below the barrier + std::atomic_signal_fence(std::memory_order_seq_cst); + // "even" phase of a tick once keys injected + shared::game_ticks++; +} + bool game_loop(bool bStartup) { uint16_t wait = bStartup ? sgGameInitInfo.nTickRate * 3 : 3; @@ -3316,6 +3471,10 @@ bool game_loop(bool bStartup) if (!gbRunGame || !gbIsMultiplayer || demo::IsRunning() || demo::IsRecording() || !nthread_has_500ms_passed()) break; } + + if (!(*GetOptions().Gameplay.shareGameStateFilename).empty()) + update_shared_state(); + return true; } diff --git a/Source/effects.cpp b/Source/effects.cpp index 838eb74c570..01e0e2d01c0 100644 --- a/Source/effects.cpp +++ b/Source/effects.cpp @@ -312,6 +312,10 @@ void effects_play_sound(SfxID id) int GetSFXLength(SfxID nSFX) { + if (!gbSndInited || !gbSoundOn) { + return 0; + } + TSFX &sfx = sgSFX[static_cast(nSFX)]; if (sfx.pSnd == nullptr) sfx.pSnd = sound_file_load(sfx.pszName.c_str(), diff --git a/Source/engine/demomode.cpp b/Source/engine/demomode.cpp index 21cb4de8bcb..81d7d179e9e 100644 --- a/Source/engine/demomode.cpp +++ b/Source/engine/demomode.cpp @@ -683,13 +683,13 @@ bool GetRunGameLoop(bool &drawGame, bool &processInput) return isGameTick; } -bool FetchMessage(SDL_Event *event, uint16_t *modState) +bool FetchMessage(SDL_Event *event, uint16_t *modState, int (*poll)(SDL_Event *event)) { - if (CurrentEventHandler == DisableInputEventHandler) + if (CurrentEventHandler.handle == DisableInputEventHandler) return false; SDL_Event e; - if (SDL_PollEvent(&e) != 0) { + if (poll(&e) != 0) { if (e.type == SDL_QUIT) { *event = e; return true; @@ -741,7 +741,7 @@ void RecordMessage(const SDL_Event &event, uint16_t modState) { if (!gbRunGame || DemoRecording == nullptr) return; - if (CurrentEventHandler == DisableInputEventHandler) + if (CurrentEventHandler.handle == DisableInputEventHandler) return; switch (event.type) { case SDL_MOUSEMOTION: diff --git a/Source/engine/demomode.h b/Source/engine/demomode.h index 59629c9d1ec..a6ad07d5375 100644 --- a/Source/engine/demomode.h +++ b/Source/engine/demomode.h @@ -22,7 +22,7 @@ bool IsRunning(); bool IsRecording(); bool GetRunGameLoop(bool &drawGame, bool &processInput); -bool FetchMessage(SDL_Event *event, uint16_t *modState); +bool FetchMessage(SDL_Event *event, uint16_t *modState, int (*poll)(SDL_Event *event)); void RecordGameLoopResult(bool runGameLoop); void RecordMessage(const SDL_Event &event, uint16_t modState); @@ -46,7 +46,7 @@ inline bool GetRunGameLoop(bool &, bool &) { return false; } -inline bool FetchMessage(SDL_Event *, uint16_t *) +inline bool FetchMessage(SDL_Event *, uint16_t *, int (*poll)(SDL_Event *event)) { return false; } diff --git a/Source/engine/events.cpp b/Source/engine/events.cpp index d4492bac9e4..d03c0163191 100644 --- a/Source/engine/events.cpp +++ b/Source/engine/events.cpp @@ -39,14 +39,15 @@ bool FalseAvail(const char *name, int value) return true; } -bool FetchMessage_Real(SDL_Event *event, uint16_t *modState) +static bool FetchMessage_Real(SDL_Event *event, uint16_t *modState, + int (*poll)(SDL_Event *event)) { #ifdef __SWITCH__ HandleDocking(); #endif SDL_Event e; - if (PollEvent(&e) == 0) { + if (poll(&e) == 0) { return false; } @@ -151,9 +152,11 @@ EventHandler SetEventHandler(EventHandler eventHandler) return previousHandler; } -bool FetchMessage(SDL_Event *event, uint16_t *modState) +bool FetchMessage(SDL_Event *event, uint16_t *modState, int (*poll)(SDL_Event *event)) { - const bool available = demo::IsRunning() ? demo::FetchMessage(event, modState) : FetchMessage_Real(event, modState); + const bool available = demo::IsRunning() ? + demo::FetchMessage(event, modState, poll) : + FetchMessage_Real(event, modState, poll); if (available && demo::IsRecording()) demo::RecordMessage(*event, *modState); @@ -163,9 +166,9 @@ bool FetchMessage(SDL_Event *event, uint16_t *modState) void HandleMessage(const SDL_Event &event, uint16_t modState) { - assert(CurrentEventHandler != nullptr); + assert(CurrentEventHandler.handle != nullptr); - CurrentEventHandler(event, modState); + CurrentEventHandler.handle(event, modState); } } // namespace devilution diff --git a/Source/engine/events.hpp b/Source/engine/events.hpp index e7d65ab5f4b..fbb3f0fc5e1 100644 --- a/Source/engine/events.hpp +++ b/Source/engine/events.hpp @@ -12,14 +12,18 @@ namespace devilution { -using EventHandler = void (*)(const SDL_Event &event, uint16_t modState); +struct EventHandler { + void (*handle)(const SDL_Event &event, uint16_t modState); + int (*poll)(SDL_Event *event); +}; /** @brief The current input handler function */ extern EventHandler CurrentEventHandler; -EventHandler SetEventHandler(EventHandler NewProc); +EventHandler SetEventHandler(EventHandler NewHandler); -bool FetchMessage(SDL_Event *event, uint16_t *modState); +bool FetchMessage(SDL_Event *event, uint16_t *modState, + int (*poll)(SDL_Event *event) = SDL_PollEvent); void HandleMessage(const SDL_Event &event, uint16_t modState); diff --git a/Source/engine/random.cpp b/Source/engine/random.cpp index 4c36129cb29..e8aa79a97a1 100644 --- a/Source/engine/random.cpp +++ b/Source/engine/random.cpp @@ -49,8 +49,11 @@ void xoshiro128plusplus::copy(state &dst, const state &src) memcpy(dst, src, sizeof(dst)); } -xoshiro128plusplus ReserveSeedSequence() +xoshiro128plusplus ReserveSeedSequence(int initialSeed) { + if (initialSeed != -1) + seedGenerator = xoshiro128plusplus((uint64_t)initialSeed); + xoshiro128plusplus reserved = seedGenerator; seedGenerator.jump(); return reserved; diff --git a/Source/engine/random.hpp b/Source/engine/random.hpp index 670b9a7c689..29ea845682c 100644 --- a/Source/engine/random.hpp +++ b/Source/engine/random.hpp @@ -285,7 +285,7 @@ class xoshiro128plusplus { /** * @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions */ -xoshiro128plusplus ReserveSeedSequence(); +xoshiro128plusplus ReserveSeedSequence(int initialSeed); /** * @brief Advances the global seed generator state and returns the new value diff --git a/Source/engine/sound.cpp b/Source/engine/sound.cpp index 1c1093578ec..bff51d3d8a0 100644 --- a/Source/engine/sound.cpp +++ b/Source/engine/sound.cpp @@ -152,6 +152,8 @@ int CapVolume(int volume) void OptionAudioChanged() { + if (HeadlessMode) + return; effects_cleanup_sfx(); music_stop(); snd_deinit(); @@ -289,6 +291,9 @@ void music_start(_music_id nTrack) { const char *trackPath; + if (HeadlessMode) + return; + assert(nTrack < NUM_MUSIC); music_stop(); if (!gbMusicOn) diff --git a/Source/gamemenu.cpp b/Source/gamemenu.cpp index aba8f7fbb36..797b78b8793 100644 --- a/Source/gamemenu.cpp +++ b/Source/gamemenu.cpp @@ -20,6 +20,7 @@ #include "pfile.h" #include "qol/floatingnumbers.h" #include "utils/language.h" +#include "utils/shared.h" #ifndef USE_SDL1 #include "controls/touch/renderers.h" @@ -291,7 +292,11 @@ void gamemenu_quit_game(bool bActivate) void gamemenu_load_game(bool /*bActivate*/) { - EventHandler saveProc = SetEventHandler(DisableInputEventHandler); + printf(">> %s:%d\n", __func__, __LINE__); + + EventHandler newHandler = { DisableInputEventHandler, SDL_PollEvent }; + EventHandler prevHandler = SetEventHandler(newHandler); + gamemenu_off(); ClearFloatingNumbers(); NewCursor(CURSOR_NONE); @@ -321,11 +326,14 @@ void gamemenu_load_game(bool /*bActivate*/) PaletteFadeIn(8); NewCursor(CURSOR_HAND); interface_msg_pump(); - SetEventHandler(saveProc); + SetEventHandler(prevHandler); + shared::game_loads++; } void gamemenu_save_game(bool /*bActivate*/) { + printf(">> %s:%d\n", __func__, __LINE__); + if (pcurs != CURSOR_HAND) { return; } @@ -335,7 +343,8 @@ void gamemenu_save_game(bool /*bActivate*/) return; } - EventHandler saveProc = SetEventHandler(DisableInputEventHandler); + EventHandler newHandler = { DisableInputEventHandler, SDL_PollEvent }; + EventHandler prevHandler = SetEventHandler(newHandler); NewCursor(CURSOR_NONE); gamemenu_off(); InitDiabloMsg(EMSG_SAVING); @@ -352,11 +361,19 @@ void gamemenu_save_game(bool /*bActivate*/) if (!demo::IsRunning()) SaveOptions(); } interface_msg_pump(); - SetEventHandler(saveProc); + SetEventHandler(prevHandler); + shared::game_saves++; } void gamemenu_on() { + if (HeadlessMode) + // Do not show any menu in headless mode, since it is not + // visible anyway. More importantly, we are not able to send + // any keys (see gmenu_presskeys() check in PressKey()), such + // as to load a game for example. + return; + isGameMenuOpen = true; if (!gbIsMultiplayer) { gmenu_set_items(sgSingleMenu, GamemenuUpdateSingle); diff --git a/Source/hwcursor.cpp b/Source/hwcursor.cpp index 0c5fa55005b..ad09a2578c0 100644 --- a/Source/hwcursor.cpp +++ b/Source/hwcursor.cpp @@ -16,6 +16,7 @@ #include "engine/clx_sprite.hpp" #include "engine/render/clx_render.hpp" #include "engine/surface.hpp" +#include "headless_mode.hpp" #include "utils/display.h" #include "utils/sdl_bilinear_scale.hpp" #include "utils/sdl_wrap.h" @@ -164,6 +165,8 @@ void SetHardwareCursor(CursorInfo cursorInfo) #if SDL_VERSION_ATLEAST(2, 0, 0) CurrentCursorInfo = cursorInfo; CurrentCursorInfo.setNeedsReinitialization(false); + if (HeadlessMode) + return; switch (cursorInfo.type()) { case CursorType::Game: #if LOG_HWCURSOR diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 4866ef831d4..66b82daf663 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -289,6 +289,7 @@ void DoLoad(interface_mode uMsg) currlevel = myPlayer.plrlevel; leveltype = GetLevelType(currlevel); IncProgress(); + //XXX loadResult = LoadGameLevel(false, ENTRY_MAIN); if (loadResult.has_value()) IncProgress(); break; @@ -480,6 +481,22 @@ void CheckShouldSkipRendering() if (!HeadlessMode) InitRendering(); } +static int ProgressEventPoll(SDL_Event *event) +{ + int ret; + + // SDL_QUIT event has higher priority + ret = SDL_PeepEvents(event, 1, SDL_GETEVENT, + SDL_QUIT, SDL_QUIT + 1); + if (ret) + return ret; + + // Peek only custom events, leaving others in the queue + return SDL_PeepEvents(event, 1, SDL_GETEVENT, + CustomEventsBegin, + CustomEventsBegin + NumCustomEvents); +} + void ProgressEventHandler(const SDL_Event &event, uint16_t modState) { DisableInputEventHandler(event, modState); @@ -525,8 +542,8 @@ void ProgressEventHandler(const SDL_Event &event, uint16_t modState) } [[maybe_unused]] EventHandler prevHandler = SetEventHandler(ProgressEventHandlerState.prevHandler); - assert(prevHandler == ProgressEventHandler); - ProgressEventHandlerState.prevHandler = nullptr; + assert(prevHandler.handle == ProgressEventHandler); + ProgressEventHandlerState.prevHandler.handle = nullptr; IsProgress = false; Player &myPlayer = *MyPlayer; @@ -580,7 +597,7 @@ void interface_msg_pump() { SDL_Event event; uint16_t modState; - while (FetchMessage(&event, &modState)) { + while (FetchMessage(&event, &modState, CurrentEventHandler.poll)) { if (event.type != SDL_QUIT) { HandleMessage(event, modState); } @@ -626,7 +643,8 @@ void ShowProgress(interface_mode uMsg) gbSomebodyWonGameKludge = false; ProgressEventHandlerState.loadStartedAt = SDL_GetTicks(); - ProgressEventHandlerState.prevHandler = SetEventHandler(ProgressEventHandler); + EventHandler newHandler = { ProgressEventHandler, ProgressEventPoll }; + ProgressEventHandlerState.prevHandler = SetEventHandler(newHandler); ProgressEventHandlerState.skipRendering = true; ProgressEventHandlerState.done = false; ProgressEventHandlerState.drawnProgress = 0; @@ -684,9 +702,10 @@ void ShowProgress(interface_mode uMsg) while (true) { CheckShouldSkipRendering(); SDL_Event event; - // We use the real `PollEvent` here instead of `FetchMessage` - // to process real events rather than the recorded ones in demo mode. - while (PollEvent(&event)) { + // We call `CustonEventHandler.poll` directly instead of + // calling the `FetchMessage` to process real events rather + // than the recorded ones in demo mode. + while (CurrentEventHandler.poll(&event)) { if (!processEvent(event)) return; } #if !SDL_PUSH_EVENT_BG_THREAD_WORKS diff --git a/Source/menu.cpp b/Source/menu.cpp index 6a7241a4262..72390988d35 100644 --- a/Source/menu.cpp +++ b/Source/menu.cpp @@ -151,7 +151,7 @@ void mainmenu_loop() do { _mainmenu_selections menu = MAINMENU_NONE; - if (demo::IsRunning()) + if (demo::IsRunning() || HeadlessMode) menu = MAINMENU_SINGLE_PLAYER; else if (!UiMainMenuDialog(gszProductName, &menu, 30)) app_fatal(_("Unable to display mainmenu")); diff --git a/Source/minitext.cpp b/Source/minitext.cpp index c209a1cc366..0f492da61e4 100644 --- a/Source/minitext.cpp +++ b/Source/minitext.cpp @@ -17,6 +17,7 @@ #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" +#include "headless_mode.hpp" #include "playerdat.hpp" #include "textdat.h" #include "utils/language.h" @@ -162,7 +163,7 @@ void InitQTextMsg(_speech_id m) default: break; } - if (Speeches[m].scrlltxt) { + if (!HeadlessMode && Speeches[m].scrlltxt) { QuestLogIsOpen = false; LoadText(_(Speeches[m].txtstr)); qtextflag = true; diff --git a/Source/monster.cpp b/Source/monster.cpp index ecf6bf42e27..8ce4bc62d75 100644 --- a/Source/monster.cpp +++ b/Source/monster.cpp @@ -3285,6 +3285,9 @@ void InitLevelMonsters() tl::expected GetLevelMTypes() { + if (*GetOptions().Gameplay.noMonsters) + return {}; + RETURN_IF_ERROR(AddMonsterType(MT_GOLEM, PLACE_SPECIAL)); if (currlevel == 16) { RETURN_IF_ERROR(AddMonsterType(MT_ADVOCATE, PLACE_SCATTER)); @@ -3539,6 +3542,9 @@ void InitGolems() tl::expected InitMonsters() { + if (*GetOptions().Gameplay.noMonsters) + return {}; + if (!gbIsSpawn && !setlevel && currlevel == 16) LoadDiabMonsts(); @@ -3630,6 +3636,9 @@ tl::expected SetMapMonsters(const uint16_t *dunData, Point st Monster *AddMonster(Point position, Direction dir, size_t typeIndex, bool inMap) { + if (*GetOptions().Gameplay.noMonsters) + return nullptr; + if (ActiveMonsterCount < MaxMonsters) { Monster &monster = Monsters[ActiveMonsters[ActiveMonsterCount++]]; if (inMap) @@ -3643,6 +3652,9 @@ Monster *AddMonster(Point position, Direction dir, size_t typeIndex, bool inMap) void SpawnMonster(Point position, Direction dir, size_t typeIndex, bool startSpecialStand /*= false*/) { + if (*GetOptions().Gameplay.noMonsters) + return; + if (ActiveMonsterCount >= MaxMonsters) return; diff --git a/Source/monster.h b/Source/monster.h index c789f78d782..4f468640509 100644 --- a/Source/monster.h +++ b/Source/monster.h @@ -482,6 +482,7 @@ extern size_t LevelMonsterTypeCount; extern Monster Monsters[MaxMonsters]; extern unsigned ActiveMonsters[MaxMonsters]; extern size_t ActiveMonsterCount; +/** Tracks the total number of monsters killed per monster_id. */ extern int MonsterKillCounts[NUM_MTYPES]; extern bool sgbSaveSoundOn; diff --git a/Source/multi.cpp b/Source/multi.cpp index 7a5255dd0f9..f6743bc7c7d 100644 --- a/Source/multi.cpp +++ b/Source/multi.cpp @@ -455,6 +455,8 @@ bool InitSingle(GameData *gameData) { Players.resize(1); + printf(">> %s:%d\n", __func__, __LINE__); + if (!SNetInitializeProvider(SELCONN_LOOPBACK, gameData)) { return false; } @@ -504,6 +506,8 @@ bool InitMulti(GameData *gameData) pfile_read_player_from_save(gSaveNumber, *MyPlayer); + printf(">> %s:%d\n", __func__, __LINE__); + return true; } @@ -511,7 +515,10 @@ bool InitMulti(GameData *gameData) void InitGameInfo() { - xoshiro128plusplus gameGenerator = ReserveSeedSequence(); + const Options &options = GetOptions(); + int initialSeed = *options.Gameplay.gameAndPlayerSeed; + + xoshiro128plusplus gameGenerator = ReserveSeedSequence(initialSeed); gameGenerator.save(sgGameInitInfo.gameSeed); sgGameInitInfo.size = sizeof(sgGameInitInfo); @@ -519,7 +526,6 @@ void InitGameInfo() sgGameInitInfo.versionMajor = PROJECT_VERSION_MAJOR; sgGameInitInfo.versionMinor = PROJECT_VERSION_MINOR; sgGameInitInfo.versionPatch = PROJECT_VERSION_PATCH; - const Options &options = GetOptions(); sgGameInitInfo.nTickRate = *options.Gameplay.tickRate; sgGameInitInfo.bRunInTown = *options.Gameplay.runInTown ? 1 : 0; sgGameInitInfo.bTheoQuest = *options.Gameplay.theoQuest ? 1 : 0; diff --git a/Source/options.cpp b/Source/options.cpp index 597e401d227..bef0ade6730 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -214,6 +214,7 @@ void LoadOptions() pEntry->LoadFromIni(pCategory->GetKey()); } } + HeadlessMode = *options.Graphics.headless; ini->getUtf8Buf("Hellfire", "SItem", options.Hellfire.szItem, sizeof(options.Hellfire.szItem)); ini->getUtf8Buf("Network", "Bind Address", "0.0.0.0", options.Network.szBindAddress, sizeof(options.Network.szBindAddress)); @@ -308,6 +309,28 @@ std::string_view OptionEntryBoolean::GetValueDescription() const return value ? _("ON") : _("OFF"); } +void OptionEntryString::LoadFromIni(std::string_view category) +{ + value = ini->getString(category, key, defaultValue); +} +void OptionEntryString::SaveToIni(std::string_view category) const +{ + ini->set(category, key, value); +} +void OptionEntryString::SetValue(std::string value) +{ + this->value = value; + this->NotifyValueChanged(); +} +OptionEntryType OptionEntryString::GetType() const +{ + return OptionEntryType::String; +} +std::string_view OptionEntryString::GetValueDescription() const +{ + return value; +} + OptionEntryType OptionEntryListBase::GetType() const { return OptionEntryType::List; @@ -685,6 +708,7 @@ std::string_view OptionEntryAudioDevice::GetDeviceName(size_t index) const GraphicsOptions::GraphicsOptions() : OptionCategoryBase("Graphics", N_("Graphics"), N_("Graphics Settings")) , fullscreen("Fullscreen", OnlyIfSupportsWindowed | OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fullscreen"), N_("Display the game in windowed or fullscreen mode."), true) + , headless("Headless", OptionEntryFlags::Invisible, "", "", false) #if !defined(USE_SDL1) || defined(__3DS__) , fitToScreen("Fit to Screen", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fit to Screen"), N_("Automatically adjust the game window to your current desktop screen aspect ratio and resolution."), true) #endif @@ -746,6 +770,7 @@ std::vector GraphicsOptions::GetEntries() #ifndef __vita__ &fullscreen, #endif + &headless, #if !defined(USE_SDL1) || defined(__3DS__) &fitToScreen, #endif @@ -816,6 +841,12 @@ GameplayOptions::GameplayOptions() { FloatingNumbers::Vertical, N_("Vertical Only") }, }) , skipLoadingScreenThresholdMs("Skip loading screen threshold, ms", OptionEntryFlags::Invisible, "", "", 0) + , shareGameStateFilename("Share game state via file", OptionEntryFlags::Invisible, "", "", "") + , gameAndPlayerSeed("Game and player initial seed", OptionEntryFlags::Invisible, "", "", -1) + , gameLevel("Load player into the level", OptionEntryFlags::Invisible, "", "", 0) + , noMonsters("Disable all monsters", OptionEntryFlags::Invisible, "", "", false) + , skipAnimation("Skip animation", OptionEntryFlags::Invisible, "", "", 0) + , noMonstersAutoPursuing("Disable monsters auto-pursuing", OptionEntryFlags::Invisible, "", "", 0) { } @@ -861,6 +892,12 @@ std::vector GameplayOptions::GetEntries() &grabInput, &pauseOnFocusLoss, &skipLoadingScreenThresholdMs, + &shareGameStateFilename, + &gameAndPlayerSeed, + &gameLevel, + &noMonsters, + &skipAnimation, + &noMonstersAutoPursuing, }; } diff --git a/Source/options.h b/Source/options.h index 62c969809d2..3f8a1aec364 100644 --- a/Source/options.h +++ b/Source/options.h @@ -94,6 +94,7 @@ enum class FloatingNumbers : uint8_t { enum class OptionEntryType : uint8_t { Boolean, + String, List, Key, PadButton, @@ -177,6 +178,30 @@ class OptionEntryBoolean : public OptionEntryBase { bool value; }; +class OptionEntryString : public OptionEntryBase { +public: + OptionEntryString(std::string_view key, OptionEntryFlags flags, const char *name, const char *description, std::string defaultValue) + : OptionEntryBase(key, flags, name, description) + , defaultValue(defaultValue) + , value(defaultValue) + { + } + [[nodiscard]] std::string operator*() const + { + return value; + } + void SetValue(std::string value); + + [[nodiscard]] OptionEntryType GetType() const override; + [[nodiscard]] std::string_view GetValueDescription() const override; + void LoadFromIni(std::string_view category) override; + void SaveToIni(std::string_view category) const override; + +private: + std::string defaultValue; + std::string value; +}; + class OptionEntryListBase : public OptionEntryBase { public: [[nodiscard]] virtual size_t GetListSize() const = 0; @@ -510,6 +535,8 @@ struct GraphicsOptions : OptionCategoryBase { OptionEntryResolution resolution; /** @brief Run in fullscreen or windowed mode. */ OptionEntryBoolean fullscreen; + /** @brief Run completely headless. */ + OptionEntryBoolean headless; #if !defined(USE_SDL1) || defined(__3DS__) /** @brief Expand the aspect ratio to match the screen. */ OptionEntryBoolean fitToScreen; @@ -633,6 +660,19 @@ struct GameplayOptions : OptionCategoryBase { * Advanced option, not displayed in the UI. */ OptionEntryInt skipLoadingScreenThresholdMs; + + /** @brief Share the whole game state for AI via file */ + OptionEntryString shareGameStateFilename; + /** @brief Game and player initial seed */ + OptionEntryInt gameAndPlayerSeed; + /** @brief Load player into the level on a new game start */ + OptionEntryInt gameLevel; + /** @brief Disable all monsters. */ + OptionEntryBoolean noMonsters; + /** @brief Skip animation */ + OptionEntryInt skipAnimation; + /** @brief Disable monsters auto-pursuing */ + OptionEntryBoolean noMonstersAutoPursuing; }; struct ControllerOptions : OptionCategoryBase { diff --git a/Source/player.cpp b/Source/player.cpp index ab9e2f9659b..ebecc5c1252 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -140,6 +140,9 @@ void StartWalkAnimation(Player &player, Direction dir, bool pmWillBeCalled) skippedFrames = 2; if (pmWillBeCalled) skippedFrames += 1; + if (*GetOptions().Gameplay.skipAnimation) + skippedFrames = player._pWFrames - 1; + NewPlrAnim(player, player_graphic::Walk, dir, AnimationDistributionFlags::ProcessAnimationPending, skippedFrames); } @@ -1803,7 +1806,10 @@ void Player::getAnimationFramesAndTicksPerFrame(player_graphic graphics, int8_t switch (graphics) { case player_graphic::Stand: numberOfFrames = _pNFrames; - ticksPerFrame = 4; + if (*GetOptions().Gameplay.skipAnimation) + ticksPerFrame = 1; + else + ticksPerFrame = 4; break; case player_graphic::Walk: numberOfFrames = _pWFrames; @@ -2170,11 +2176,21 @@ void SetPlrAnims(Player &player) auto gn = static_cast(player._pgfxnum & 0xFU); if (leveltype == DTYPE_TOWN) { - player._pNFrames = plrAtkAnimData.townIdleFrames; - player._pWFrames = plrAtkAnimData.townWalkingFrames; + if (*GetOptions().Gameplay.skipAnimation) { + player._pNFrames = 1; + player._pWFrames = 1; + } else { + player._pNFrames = plrAtkAnimData.townIdleFrames; + player._pWFrames = plrAtkAnimData.townWalkingFrames; + } } else { - player._pNFrames = plrAtkAnimData.idleFrames; - player._pWFrames = plrAtkAnimData.walkingFrames; + if (*GetOptions().Gameplay.skipAnimation) { + player._pNFrames = 1; + player._pWFrames = 1; + } else { + player._pNFrames = plrAtkAnimData.idleFrames; + player._pWFrames = plrAtkAnimData.walkingFrames; + } player._pHFrames = plrAtkAnimData.recoveryFrames; player._pBFrames = plrAtkAnimData.blockingFrames; switch (gn) { @@ -2235,8 +2251,14 @@ void SetPlrAnims(Player &player) */ void CreatePlayer(Player &player, HeroClass c) { + int initialSeed = *GetOptions().Gameplay.gameAndPlayerSeed; + player = {}; - SetRndSeed(SDL_GetTicks()); + + if (initialSeed != -1) + SetRndSeed(initialSeed); + else + SetRndSeed(SDL_GetTicks()); player.setCharacterLevel(1); player._pClass = c; diff --git a/Source/storm/storm_net.cpp b/Source/storm/storm_net.cpp index 72bf4498bda..f5f689a4bd3 100644 --- a/Source/storm/storm_net.cpp +++ b/Source/storm/storm_net.cpp @@ -15,6 +15,7 @@ #include "engine/demomode.h" #include "headless_mode.hpp" #include "menu.h" +#include "pfile.h" #include "options.h" #include "utils/stubs.h" #include "utils/utf8.hpp" @@ -128,7 +129,30 @@ bool SNetInitializeProvider(uint32_t provider, struct GameData *gameData) std::lock_guard lg(storm_net_mutex); #endif dvlnet_inst = net::abstract_net::MakeNet(provider); - return (HeadlessMode && !demo::IsRunning()) || mainmenu_select_hero_dialog(gameData); + if (HeadlessMode && !demo::IsRunning()) { + devilution::_uidefaultstats defaults; + devilution::_uiheroinfo heroInfo; + HeroClass heroClass = HeroClass::Warrior; + + pfile_ui_set_class_stats(heroClass, &defaults); + + heroInfo.level = 1; + heroInfo.heroclass = heroClass; + heroInfo.strength = defaults.strength; + heroInfo.magic = defaults.magic; + heroInfo.dexterity = defaults.dexterity; + heroInfo.vitality = defaults.vitality; + strcpy(heroInfo.name, "AI Agent"); + heroInfo.saveNumber = 0; + pfile_ui_save_create(&heroInfo); + + gSaveNumber = heroInfo.saveNumber; + gameData->nDifficulty = DIFF_NORMAL; + + return true; + } else { + return mainmenu_select_hero_dialog(gameData); + } } /** diff --git a/Source/utils/log.hpp b/Source/utils/log.hpp index 3876581552d..4b61ce12352 100644 --- a/Source/utils/log.hpp +++ b/Source/utils/log.hpp @@ -59,6 +59,7 @@ std::string format(std::string_view fmt, Args &&...args) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "%s", error.c_str()); app_fatal(error); #endif + return ""; } } diff --git a/Source/utils/mapping.cpp b/Source/utils/mapping.cpp new file mode 100644 index 00000000000..544380dbe06 --- /dev/null +++ b/Source/utils/mapping.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include "utils/ring.h" +#include "utils/shared.h" + +extern char __bss_start; +extern char _end; + +namespace devilution { + +/* + * Here we define all variables which should be shared + */ +namespace shared { + struct ring_queue input_queue; + struct ring_queue events_queue; + struct Player player; + uint64_t game_ticks; + uint64_t game_saves; + uint64_t game_loads; +} + +/** + * share_diablo_state() - finds all mapped regions where '__bss_start' + * and '_end' lie, expecting these regions to have 'rw-p' permissions + * and an executable binary as a mapped file. These mapped regions + * will be remapped to @mshared_path, so that an external application + * can access the '.data' and '.bss' sections of this application. + */ +void shared::share_diablo_state(const std::string &mshared_path) +{ +#ifdef HAVE_LINKER_BSS_SYMBOLS + // This is the simplest way to determine the region boundaries of + // the .data and .bss sections without parsing the ELF itself. It + // should work for most architectures where the binary is built + // using GNU tools. + uintptr_t region_start = (uintptr_t)&__bss_start; + uintptr_t region_end = (uintptr_t)&_end; + + char exe_path[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + exe_path[len] = '\0'; + + FILE *fp = fopen("/proc/self/maps", "r"); + if (!fp) { + perror("fopen"); + exit(1); + } + + uintptr_t start = 0, end = 0, offset = 0; + char line[4096]; + int found = 0; + + while (fgets(line, sizeof(line), fp)) { + uintptr_t s, e, o; + char perms[5]; + char path[256]; + int n; + + path[0] = '\0'; + perms[0] = '\0'; + + n = sscanf(line, "%lx-%lx %4s %lx %*s %*s %[^\n]", &s, &e, perms, &o, path); + if (n < 4) { + // Wow, can't parse maps? + fprintf(stderr, "Can't parse maps\n"); + fclose(fp); + exit(1); + } + if (!found && s <= region_start && region_start < e) { + if (strcmp(perms, "rw-p") || strcmp(path, exe_path)) { + // Expect 'rw' permissions and mapping of an exe binary + fprintf(stderr, "Can't find correct mapped region: incorrect permissions or mapping path\n"); + fclose(fp); + exit(1); + } + start = s; + end = e; + offset = o; + if (s < region_end && region_end <= e) { + // One mapped region fits everything + found = 2; + break; + } + found = 1; + } else if (found) { + if (strcmp(perms, "rw-p")) { + // Expect 'rw' permissions and mapping of an exe binary + fprintf(stderr, "Can't find correct mapped region: incorrect permissions\n"); + fclose(fp); + exit(1); + } + if (end != s) { + // Expect contiguous regions + fprintf(stderr, "%lx %lx\n", s, end); + fprintf(stderr, "Can't find correct mapped region: regions must be contiguous\n"); + fclose(fp); + exit(1); + } + end = e; + if (s < region_end && region_end <= e) { + found = 2; + break; + } + } + } + fclose(fp); + + if (!start || !end || found != 2) { + fprintf(stderr, "Could not find suitable contiguous mappings\n"); + exit(1); + } + + int fd = open(mshared_path.c_str(), O_CREAT | O_RDWR, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); + if (fd < 0) { + perror("open"); + exit(1); + } + + size_t length = end - start; + if (ftruncate(fd, length) < 0) { + perror("ftruncate"); + close(fd); + exit(1); + } + + // Write current memory content to the file + if (write(fd, (void *)start, length) != (ssize_t)length) { + perror("write"); + close(fd); + exit(1); + } + + // Remap the entire region as shared with 'rw' permissions + void *new_map = mmap((void *)start, length, PROT_READ | PROT_WRITE, + MAP_FIXED | MAP_SHARED, fd, 0); + if (new_map == MAP_FAILED) { + perror("mmap"); + close(fd); + exit(1); + } + + printf("Remapped region %lx-%lx (offset %lx, length %lx) as MAP_SHARED\n", + start, end, offset, length); + close(fd); +#else // HAVE_LINKER_BSS_SYMBOLS + fprintf(stderr, "Sharing of internal state is unsupported due to the absence of linker BSS symbols.\n"); + exit(1); +#endif +} + +} diff --git a/Source/utils/mapping.h b/Source/utils/mapping.h new file mode 100644 index 00000000000..771a11a62d4 --- /dev/null +++ b/Source/utils/mapping.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace devilution { + +void share_diablo_state(const std::string &path); + +} diff --git a/Source/utils/ring.h b/Source/utils/ring.h new file mode 100644 index 00000000000..6b003e9c0b8 --- /dev/null +++ b/Source/utils/ring.h @@ -0,0 +1,144 @@ +#ifndef RING_H +#define RING_H + +#include +#include +#include + +#define RING_QUEUE_CAPACITY 128 +#define RING_QUEUE_MASK (RING_QUEUE_CAPACITY - 1) + +enum ring_entry_type { + RING_ENTRY_KEY_LEFT = 1<<0, + RING_ENTRY_KEY_RIGHT = 1<<1, + RING_ENTRY_KEY_UP = 1<<2, + RING_ENTRY_KEY_DOWN = 1<<3, + RING_ENTRY_KEY_X = 1<<4, + RING_ENTRY_KEY_Y = 1<<5, + RING_ENTRY_KEY_A = 1<<6, + RING_ENTRY_KEY_B = 1<<7, + RING_ENTRY_KEY_SAVE = 1<<8, + RING_ENTRY_KEY_LOAD = 1<<9, + RING_ENTRY_KEY_PAUSE = 1<<10, + + RING_ENTRY_F_SINGLE_TICK_PRESS = 1<<31, + + RING_ENTRY_FLAGS = (RING_ENTRY_F_SINGLE_TICK_PRESS) +}; + +struct ring_entry { + uint32_t type; + uint32_t data; +}; + +struct ring_queue { + uint32_t write_idx; + uint32_t read_idx; + struct ring_entry array[RING_QUEUE_CAPACITY]; +}; + +static inline void ring_queue_init(struct ring_queue *ring) +{ + *ring = (struct ring_queue){}; +} + +static inline bool +ring_queue_has_capacity_to_submit(struct ring_queue *ring) +{ + return (ring->write_idx - ring->read_idx < RING_QUEUE_CAPACITY); +} + +static inline struct ring_entry * +ring_queue_get_entry_to_submit(struct ring_queue *ring) +{ + return &ring->array[ring->write_idx & RING_QUEUE_MASK]; +} + +static inline void ring_queue_submit(struct ring_queue *ring) +{ + __atomic_store_n(&ring->write_idx, ring->write_idx + 1, + __ATOMIC_RELEASE); +} + +static inline struct ring_entry * +ring_queue_get_entry_to_retreive(struct ring_queue *ring) +{ + unsigned write_idx; + + write_idx = __atomic_load_n(&ring->write_idx, __ATOMIC_ACQUIRE); + if (write_idx == ring->read_idx) + return NULL; + + return &ring->array[ring->read_idx & RING_QUEUE_MASK]; +} + +static inline void ring_queue_retrieve(struct ring_queue *ring) +{ + ring->read_idx++; +} + +/* + * To build a test from the header: + * gcc -std=c99 -Wall -x c -o ring ring.h -DTEST + */ +#ifdef TEST + +#include + +int main() +{ + struct ring_queue ring; + struct ring_entry *entry, *entry2; + bool res; + int i; + + ring_queue_init(&ring); + + entry = ring_queue_get_entry_to_retreive(&ring); + assert(entry == NULL); + + res = ring_queue_has_capacity_to_submit(&ring); + assert(res); + + /* Consume the whole queue capacity */ + i = 0; + while (ring_queue_has_capacity_to_submit(&ring)) { + entry = ring_queue_get_entry_to_submit(&ring); + entry->type = i; + ring_queue_submit(&ring); + i++; + } + assert(i == RING_QUEUE_CAPACITY); + + /* Retrieve everything */ + i = 0; + while (1) { + entry = ring_queue_get_entry_to_retreive(&ring); + if (!entry) + break; + assert(entry->type == i); + ring_queue_retrieve(&ring); + i++; + } + assert(i == RING_QUEUE_CAPACITY); + + entry = ring_queue_get_entry_to_submit(&ring); + assert(entry != NULL); + entry->type = 666; + + ring_queue_submit(&ring); + entry = ring_queue_get_entry_to_retreive(&ring); + assert(entry != NULL); + assert(entry->type == 666); + + entry2 = ring_queue_get_entry_to_retreive(&ring); + assert(entry == entry2); + + ring_queue_retrieve(&ring); + entry = ring_queue_get_entry_to_retreive(&ring); + assert(entry == NULL); +} + +#endif /* TEST */ + +#endif /* RING_H */ diff --git a/Source/utils/shared.h b/Source/utils/shared.h new file mode 100644 index 00000000000..3982c8eed7c --- /dev/null +++ b/Source/utils/shared.h @@ -0,0 +1,18 @@ +#pragma once + +#include "utils/ring.h" +#include "player.h" + +namespace devilution { + +namespace shared { + extern struct ring_queue input_queue; + extern struct ring_queue events_queue; + extern uint64_t game_ticks; + extern uint64_t game_saves; + extern uint64_t game_loads; + extern struct Player player; + + void share_diablo_state(const std::string &path); +} +}