diff --git a/data/base/script/campaign/libcampaign_includes/artifact.js b/data/base/script/campaign/libcampaign_includes/artifact.js index 6924fdd5188..637d6598106 100644 --- a/data/base/script/campaign/libcampaign_includes/artifact.js +++ b/data/base/script/campaign/libcampaign_includes/artifact.js @@ -221,6 +221,12 @@ function __camPickupArtifact(artifact) camTrace("Artifact", artifact.id, "is not managed"); return; } + if (Object.hasOwn(ai, "pickedUp") && ai.pickedUp === true) + { + camTrace("Already picked up the artifact", __ALABEL); + return; + } + ai.pickedUp = true; camTrace("Picked up", ai.tech); playSound(cam_sounds.artifactRecovered, artifact.x, artifact.y, artifact.z); diff --git a/src/game.cpp b/src/game.cpp index e5bad4c6a4e..b24dae91815 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -95,6 +95,7 @@ #include "multigifts.h" #include "wzscriptdebug.h" #include "gamehistorylogger.h" +#include "wzapi.h" #include #if defined(__clang__) @@ -3379,7 +3380,7 @@ bool saveGame(const char *aFileName, GAME_TYPE saveType, bool isAutoSave) size_t fileExtension; char CurrentFileName[PATH_MAX] = {'\0'}; - triggerEvent(TRIGGER_GAME_SAVING); + executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_GAME_SAVING); }); ASSERT_OR_RETURN(false, aFileName && strlen(aFileName) > 4, "Bad savegame filename"); sstrcpy(CurrentFileName, aFileName); diff --git a/src/init.cpp b/src/init.cpp index 4484d4626bd..0d4212f9d67 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -99,6 +99,7 @@ #include "seqdisp.h" #include "version.h" #include "hci/teamstrategy.h" +#include "wzapi.h" #include #include @@ -1895,7 +1896,7 @@ bool stageThreeInitialise() if (getLevelLoadType() == GTYPE_SAVE_MIDMISSION || getLevelLoadType() == GTYPE_SAVE_START) { - triggerEvent(TRIGGER_GAME_LOADED); + executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_GAME_LOADED); }); } else { @@ -1909,7 +1910,7 @@ bool stageThreeInitialise() gInputManager.contexts().set(InputContext::DEBUG_MISC, InputContext::State::ACTIVE); } - triggerEvent(TRIGGER_GAME_INIT); + executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_GAME_INIT); }); playerBuiltHQ = structureExists(selectedPlayer, REF_HQ, true, false); } diff --git a/src/keybind.cpp b/src/keybind.cpp index 287404790ce..ac7bf669215 100644 --- a/src/keybind.cpp +++ b/src/keybind.cpp @@ -387,49 +387,61 @@ void kf_CloneSelected(int limit) return; // no-op } - mutating_list_iterate(apsDroidLists[selectedPlayer], [limit, &sTemplate](DROID* d) + bool selectedAnything = false; + const DROID* droidToClone = nullptr; + for (const DROID* d : apsDroidLists[selectedPlayer]) { - if (d->selected) + if (!d->selected) { - enumerateTemplates(selectedPlayer, [psDroid = d, &sTemplate](DROID_TEMPLATE* psTempl) { - if (psTempl->name.compare(psDroid->aName) == 0) - { - sTemplate = psTempl; - return false; // stop enumerating - } - return true; - }); - - if (!sTemplate) + continue; + } + selectedAnything = true; + enumerateTemplates(selectedPlayer, [psDroid = d, &sTemplate](DROID_TEMPLATE* psTempl) { + if (psTempl->name.compare(psDroid->aName) == 0) { - debug(LOG_ERROR, "Cloning vat has been destroyed. We can't find the template for this droid: %s, id:%u, type:%d!", d->aName, d->id, d->droidType); - return IterationResult::BREAK_ITERATION; + sTemplate = psTempl; + return false; // stop enumerating } - - // create a new droid army - for (int i = 0; i < limit; i++) - { - Vector2i pos = d->pos.xy() + iSinCosR(40503 * i, iSqrt(50 * 50 * (i + 1))); // 40503 = 65536/φ (A bit more than a right angle) - DROID* psNewDroid = buildDroid(sTemplate, pos.x, pos.y, d->player, false, nullptr); - if (psNewDroid) - { - addDroid(psNewDroid, apsDroidLists); - triggerEventDroidBuilt(psNewDroid, nullptr); - } - else if (!bMultiMessages) - { - debug(LOG_ERROR, "Cloning has failed for template:%s id:%d", getID(sTemplate), sTemplate->multiPlayerID); - } - } - std::string msg = astringf(_("Player %u is cheating a new droid army of: %d × %s."), selectedPlayer, limit, d->aName); - sendInGameSystemMessage(msg.c_str()); - Cheated = true; - audio_PlayTrack(ID_SOUND_NEXUS_LAUGH1); - return IterationResult::BREAK_ITERATION; + return true; + }); + if (!sTemplate) + { + debug(LOG_ERROR, "Cloning vat has been destroyed. We can't find the template for this droid: %s, id:%u, type:%d!", d->aName, d->id, d->droidType); + break; } + droidToClone = d; + // Break out of the loop if we've successfully found the associated droid template. + break; + } + if (!selectedAnything) + { debug(LOG_INFO, "Nothing was selected?"); - return IterationResult::CONTINUE_ITERATION; - }); + return; + } + if (!sTemplate) + { + return; + } + ASSERT(droidToClone != nullptr, "No droid to clone found"); + // create a new droid army + for (int i = 0; i < limit; i++) + { + Vector2i pos = droidToClone->pos.xy() + iSinCosR(40503 * i, iSqrt(50 * 50 * (i + 1))); // 40503 = 65536/φ (A bit more than a right angle) + DROID* psNewDroid = buildDroid(sTemplate, pos.x, pos.y, droidToClone->player, false, nullptr); + if (psNewDroid) + { + addDroid(psNewDroid, apsDroidLists); + triggerEventDroidBuilt(psNewDroid, nullptr); + } + else if (!bMultiMessages) + { + debug(LOG_ERROR, "Cloning has failed for template:%s id:%d", getID(sTemplate), sTemplate->multiPlayerID); + } + } + std::string msg = astringf(_("Player %u is cheating a new droid army of: %d × %s."), selectedPlayer, limit, droidToClone->aName); + sendInGameSystemMessage(msg.c_str()); + Cheated = true; + audio_PlayTrack(ID_SOUND_NEXUS_LAUGH1); } void kf_MakeMeHero() diff --git a/src/loop.cpp b/src/loop.cpp index da0ac6e0de9..3a1a29deb90 100644 --- a/src/loop.cpp +++ b/src/loop.cpp @@ -87,6 +87,7 @@ #include "clparse.h" #include "gamehistorylogger.h" #include "profiling.h" +#include "wzapi.h" #include "warzoneconfig.h" @@ -515,7 +516,7 @@ static void gameStateUpdate() if (!paused && !scriptPaused()) { - updateScripts(); + executeFnAndProcessScriptQueuedRemovals([]() { updateScripts(); }); } // Update abandoned structures @@ -544,33 +545,40 @@ static void gameStateUpdate() //update the current power available for a player updatePlayerPower(i); - mutating_list_iterate(apsDroidLists[i], [](DROID* d) - { - droidUpdate(d); - return IterationResult::CONTINUE_ITERATION; + executeFnAndProcessScriptQueuedRemovals([i]() { + mutating_list_iterate(apsDroidLists[i], [](DROID* d) + { + droidUpdate(d); + return IterationResult::CONTINUE_ITERATION; + }); }); - mutating_list_iterate(mission.apsDroidLists[i], [](DROID* d) - { - missionDroidUpdate(d); - return IterationResult::CONTINUE_ITERATION; + executeFnAndProcessScriptQueuedRemovals([i]() { + mutating_list_iterate(mission.apsDroidLists[i], [](DROID* d) + { + missionDroidUpdate(d); + return IterationResult::CONTINUE_ITERATION; + }); }); - // FIXME: These for-loops are code duplication - mutating_list_iterate(apsStructLists[i], [](STRUCTURE* s) - { - structureUpdate(s, false); - return IterationResult::CONTINUE_ITERATION; + executeFnAndProcessScriptQueuedRemovals([i]() { + mutating_list_iterate(apsStructLists[i], [](STRUCTURE* s) + { + structureUpdate(s, false); + return IterationResult::CONTINUE_ITERATION; + }); }); - mutating_list_iterate(mission.apsStructLists[i], [](STRUCTURE* s) - { - structureUpdate(s, true); // update for mission - return IterationResult::CONTINUE_ITERATION; + executeFnAndProcessScriptQueuedRemovals([i]() { + mutating_list_iterate(mission.apsStructLists[i], [](STRUCTURE* s) + { + structureUpdate(s, true); // update for mission + return IterationResult::CONTINUE_ITERATION; + }); }); } missionTimerUpdate(); - proj_UpdateAll(); + executeFnAndProcessScriptQueuedRemovals([]() { proj_UpdateAll(); }); for (FEATURE *psCFeat : apsFeatureLists[0]) { @@ -641,7 +649,7 @@ GAMECODE gameLoop() { // Receive NET_BLAH messages. // Receive GAME_BLAH messages, and if it's time, process exactly as many GAME_BLAH messages as required to be able to tick the gameTime. - recvMessage(); + executeFnAndProcessScriptQueuedRemovals([]() { recvMessage(); }); bool selectedPlayerIsSpectator = bMultiPlayer && NetPlay.players[selectedPlayer].isSpectator; bool multiplayerHostDisconnected = bMultiPlayer && !NetPlay.isHostAlive && NetPlay.bComms && !NetPlay.isHost; // do not fast-forward after the host has disconnected @@ -694,10 +702,16 @@ GAMECODE gameLoop() NETflush(); // Make sure that we aren't waiting too long to send data. } - unsigned before = wzGetTicks(); - GAMECODE renderReturn = renderLoop(); - pie_ScreenFrameRenderEnd(); // must happen here for proper renderBudget calculation - unsigned after = wzGetTicks(); + unsigned before, after; + GAMECODE renderReturn; + executeFnAndProcessScriptQueuedRemovals([&before, &after, &renderReturn]() + { + before = wzGetTicks(); + renderReturn = renderLoop(); + pie_ScreenFrameRenderEnd(); // must happen here for proper renderBudget calculation + after = wzGetTicks(); + }); + #if defined(__EMSCRIPTEN__) lastRenderDelta = (after - before); @@ -753,7 +767,7 @@ void videoLoop() { displayGameOver(getScriptWinLoseVideo() == PLAY_WIN, false); } - triggerEvent(TRIGGER_VIDEO_QUIT); + executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_VIDEO_QUIT); }); } } } diff --git a/src/main.cpp b/src/main.cpp index b3d683fb45e..4ba4f3e621e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -116,6 +116,7 @@ #include "wzcrashhandlingproviders.h" #include "wzpropertyproviders.h" #include "3rdparty/gsl_finally.h" +#include "wzapi.h" #if defined(WZ_OS_UNIX) # include @@ -987,7 +988,7 @@ static void startGameLoop() { addMissionTimerInterface(); } - triggerEvent(TRIGGER_START_LEVEL); + executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_START_LEVEL); }); screen_disableMapPreview(); if (!bMultiPlayer && getCamTweakOption_AutosavesOnly()) diff --git a/src/mission.cpp b/src/mission.cpp index 547449148fe..cc645b7af48 100644 --- a/src/mission.cpp +++ b/src/mission.cpp @@ -82,6 +82,7 @@ #include "lib/framework/wztime.h" #include "keybind.h" #include "campaigninfo.h" +#include "wzapi.h" #define IDMISSIONRES_TXT 11004 #define IDMISSIONRES_LOAD 11005 @@ -2113,7 +2114,10 @@ void intUpdateTransporterTimer(WIDGET *psWidget, const W_CONTEXT *psContext) if (psTransporter->action == DACTION_TRANSPORTWAITTOFLYIN) { missionFlyTransportersIn(selectedPlayer, false); - triggerEvent(TRIGGER_TRANSPORTER_ARRIVED, psTransporter); + executeFnAndProcessScriptQueuedRemovals([psTransporter]() + { + triggerEvent(TRIGGER_TRANSPORTER_ARRIVED, psTransporter); + }); } } } @@ -2859,7 +2863,7 @@ void missionTimerUpdate() if ((SDWORD)(gameTime - mission.startTime) > mission.time) { //the script can call the end game cos have failed! - triggerEvent(TRIGGER_MISSION_TIMEOUT); + executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_MISSION_TIMEOUT); }); } } } diff --git a/src/transporter.cpp b/src/transporter.cpp index 85dd3fca08a..409eb5f085d 100644 --- a/src/transporter.cpp +++ b/src/transporter.cpp @@ -56,6 +56,7 @@ #include "visibility.h" #include "multiplay.h" #include "hci/groups.h" +#include "wzapi.h" //#define IDTRANS_FORM 9000 //The Transporter base form #define IDTRANS_CLOSE 9002 //The close button icon @@ -1280,7 +1281,7 @@ void processLaunchTransporter() //set the data for the transporter timer widgSetUserData(psWScreen, IDTRANTIMER_DISPLAY, (void *)psCurrTransporter); - triggerEvent(TRIGGER_TRANSPORTER_LAUNCH, psCurrTransporter); + executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_TRANSPORTER_LAUNCH, psCurrTransporter); }); } } } diff --git a/src/wzapi.cpp b/src/wzapi.cpp index 556a2534ff7..3da327a6c45 100644 --- a/src/wzapi.cpp +++ b/src/wzapi.cpp @@ -2930,36 +2930,22 @@ bool wzapi::removeStruct(WZAPI_PARAMS(STRUCTURE *psStruct)) WZAPI_DEPRECATED //-- ## removeObject(gameObject[, sfx]) //-- -//-- Remove the given game object with special effects. Returns a boolean that is true on success. +//-- Queue the given game object for removal with or without special effects. Returns a boolean that is true on success. //-- A second, optional boolean parameter specifies whether special effects are to be applied. (3.2+ only) //-- +//-- BREAKING CHANGE (4.5+): the effect of this function is not immediate anymore, the object will be +//-- queued for later removal instead of destroying it right away. +//-- User scripts should not rely on this function having immediate side-effects. +//-- bool wzapi::removeObject(WZAPI_PARAMS(BASE_OBJECT *psObj, optional _sfx)) { SCRIPT_ASSERT(false, context, psObj, "No valid object provided"); - bool sfx = _sfx.value_or(false); + SCRIPT_ASSERT(false, context, + psObj->type == OBJ_STRUCTURE || psObj->type == OBJ_DROID || psObj->type == OBJ_FEATURE, + "Wrong game object type"); - bool retval = false; - if (sfx) - { - switch (psObj->type) - { - case OBJ_STRUCTURE: destroyStruct((STRUCTURE *)psObj, gameTime); break; - case OBJ_DROID: retval = destroyDroid((DROID *)psObj, gameTime); break; - case OBJ_FEATURE: retval = destroyFeature((FEATURE *)psObj, gameTime); break; - default: SCRIPT_ASSERT(false, context, false, "Wrong game object type"); break; - } - } - else - { - switch (psObj->type) - { - case OBJ_STRUCTURE: retval = removeStruct((STRUCTURE *)psObj, true); break; - case OBJ_DROID: retval = removeDroidBase((DROID *)psObj); break; - case OBJ_FEATURE: retval = removeFeature((FEATURE *)psObj); break; - default: SCRIPT_ASSERT(false, context, false, "Wrong game object type"); break; - } - } - return retval; + scriptQueuedObjectRemovals().emplace_back(psObj, _sfx.value_or(false)); + return true; } //-- ## setScrollLimits(x1, y1, x2, y2) @@ -4600,3 +4586,50 @@ nlohmann::json wzapi::constructMapTilesArray() } return mapTileArray; } + +wzapi::QueuedObjectRemovalsVector& wzapi::scriptQueuedObjectRemovals() +{ + static QueuedObjectRemovalsVector instance = []() + { + static constexpr size_t initialCapacity = 32; + QueuedObjectRemovalsVector ret; + ret.reserve(initialCapacity); + return ret; + }(); + return instance; +} + +void wzapi::processScriptQueuedObjectRemovals() +{ + auto& queuedObjRemovals = scriptQueuedObjectRemovals(); + for (auto& objWithSfxFlag : queuedObjRemovals) + { + BASE_OBJECT* psObj = objWithSfxFlag.first; + if (psObj->died) + { + debug(LOG_MSG, "Object %p is already dead, not processing", static_cast(psObj)); + continue; + } + if (objWithSfxFlag.second) + { + switch (psObj->type) + { + case OBJ_STRUCTURE: destroyStruct((STRUCTURE*)psObj, gameTime); break; + case OBJ_DROID: destroyDroid((DROID*)psObj, gameTime); break; + case OBJ_FEATURE: destroyFeature((FEATURE*)psObj, gameTime); break; + default: ASSERT(false, "Wrong game object type"); break; + } + } + else + { + switch (psObj->type) + { + case OBJ_STRUCTURE: removeStruct((STRUCTURE*)psObj, true); break; + case OBJ_DROID: removeDroidBase((DROID*)psObj); break; + case OBJ_FEATURE: removeFeature((FEATURE*)psObj); break; + default: ASSERT(false, "Wrong game object type"); break; + } + } + } + queuedObjRemovals.clear(); +} diff --git a/src/wzapi.h b/src/wzapi.h index 21f958efd79..6fbeb52b6d9 100644 --- a/src/wzapi.h +++ b/src/wzapi.h @@ -1098,6 +1098,14 @@ namespace wzapi no_return_value makeComponentAvailable(WZAPI_PARAMS(std::string componentName, int player)); bool allianceExistsBetween(WZAPI_PARAMS(int player1, int player2)); bool removeStruct(WZAPI_PARAMS(STRUCTURE *psStruct)) WZAPI_DEPRECATED; + /// + /// Queues the given game object for removal with or without special effects. + /// + /// The current behavior is in effect since version 4.5+. + /// + /// Game object to be queued for removal. + /// Optional boolean parameter that specifies whether special effects are to be applied. + /// `true` if `psObj` has been successfully queued for destruction. bool removeObject(WZAPI_PARAMS(BASE_OBJECT *psObj, optional _sfx)); no_return_value setScrollLimits(WZAPI_PARAMS(int x1, int y1, int x2, int y2)); scr_area getScrollLimits(WZAPI_NO_PARAMS); @@ -1126,6 +1134,34 @@ namespace wzapi nlohmann::json constructStaticPlayerData(); std::vector getUpgradesObject(); nlohmann::json constructMapTilesArray(); + + // `wzapi::removeObject` API call adds the game objects to this list. + // They will eventually be released during calls to `processScriptQueuedObjectRemovals()` + // during the following stages of the main game loop: + // 1. `recvMessage()` - there are a few functions which distribute + // resources of the defeated players among others and may trigger script events. + // 2. `updateScripts()` - processes queued timer functions. + // 3. `droidUpdate()`, `missionDroidUpdate()`, `structureUpdate()` - main + // routines for updating the state of in-game objects. These would potentially + // trigger the majority of script events. + // + // Each pair represents tuple. + using QueuedObjectRemovalsVector = std::vector>; + + QueuedObjectRemovalsVector& scriptQueuedObjectRemovals(); + /// + /// Walks `scriptQueuedObjectRemovals()` list, destroys every object in that list + /// and clears the container. + /// + void processScriptQueuedObjectRemovals(); +} // namespace wzapi + +template +static void executeFnAndProcessScriptQueuedRemovals(Fn fn) +{ + ASSERT(wzapi::scriptQueuedObjectRemovals().empty(), "Leftover script-queued object removals detected!"); + fn(); + wzapi::processScriptQueuedObjectRemovals(); } #endif