diff --git a/CREDITS.md b/CREDITS.md index 6a2035cd83..293b8fa316 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -153,6 +153,7 @@ This page lists all the individual contributions to the project by their author. - Event 606: AttachEffect is attaching to a Techno - Linked superweapons - Unit & infantry auto-conversion on ammo change + - Force the check of events in sequential order - **Starkku**: - Misc. minor bugfixes & improvements - AI script actions: diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 2def0db9a8..0b20114b16 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -23,6 +23,7 @@ + @@ -218,6 +219,7 @@ + diff --git a/YRpp b/YRpp index 5af96790ce..beb80e8fbd 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit 5af96790ce73e4ea068a390c60c124dccbc220e1 +Subproject commit beb80e8fbd48cd2d016ffaccb7f2c3476ef728d5 diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 4e31d7e494..bdc0d975c0 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -881,3 +881,18 @@ In `mycampaign.map`: ID=EventCount,...,606,2,0,[AttachEffectType],... ... ``` + +### `1000` Force the check of events in sequential order + +- By default, the game evaluates all map triggers in parallel. Adding this map event forces short-circuit evaluation as soon as any subsequent event returns `false`. +- This only affects evaluation from this control event (inclusive) to the last event in the list. +- All events placed before this control event still work in a non-short-circuiting manner. +- Sequential events will not begin evaluation until all preceding events have successfully completed. + +In `mycampaign.map`: +```ini +[Events] +... +ID=EventCount,...,1000,0,0,0,... +... +``` diff --git a/docs/Whats-New.md b/docs/Whats-New.md index f550b9df7d..bb9543c9e5 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -464,6 +464,7 @@ New: - [Customize if cloning need power](Fixed-or-Improved-Logics.md#customize-if-cloning-need-power) (by NetsuNegi) - [Added Target Filtering Options to AttachEffect System](New-or-Enhanced-Logics.md#attached-effects) (by Flactine) - [Customize type selection for IFV](Fixed-or-Improved-Logics.md#customize-type-selection-for-ifv) (by NetsuNegi) +- Force the check of events in sequential order (by FS-21) - CellSpread in cylinder shape (by TaranDahl) - CellSpread damage check if victim is in air or on floor (by TaranDahl) diff --git a/src/Ext/TEvent/Body.h b/src/Ext/TEvent/Body.h index 553494e697..994286c564 100644 --- a/src/Ext/TEvent/Body.h +++ b/src/Ext/TEvent/Body.h @@ -56,6 +56,8 @@ enum PhobosTriggerEvent CellHasAnyTechnoTypeFromList = 605, AttachedIsUnderAttachedEffect = 606, + ForceSequentialEvents = 1000, + _DummyMaximum, }; diff --git a/src/Ext/Trigger/Body.cpp b/src/Ext/Trigger/Body.cpp new file mode 100644 index 0000000000..2b66f83dbd --- /dev/null +++ b/src/Ext/Trigger/Body.cpp @@ -0,0 +1,101 @@ +#include "Body.h" + +#include +#include +#include + +#include +#include + +//Static init +TriggerExt::ExtContainer TriggerExt::ExtMap; + +// ============================= +// load / save + +template +void TriggerExt::ExtData::Serialize(T& Stm) +{ + Stm + .Process(this->SortedEventsList) + .Process(this->SequentialTimers) + .Process(this->SequentialTimersOriginalValue) + .Process(this->ParallelTimers) + .Process(this->ParallelTimersOriginalValue) + .Process(this->SequentialSwitchModeIndex) + ; +} + +void TriggerExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) +{ + Extension::LoadFromStream(Stm); + this->Serialize(Stm); +} + +void TriggerExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) +{ + Extension::SaveToStream(Stm); + this->Serialize(Stm); +} + +DEFINE_HOOK(0x7260C8, TriggerClass_CTOR, 0x8) +{ + GET(TriggerClass*, pItem, ESI); + + TriggerExt::ExtMap.Allocate(pItem); + + return 0; +} + +DEFINE_HOOK(0x72617D, TriggerClass_DTOR, 0xF) +{ + GET(TriggerClass*, pItem, ESI); + + TriggerExt::ExtMap.Remove(pItem); + + return 0; +} + +DEFINE_HOOK(0x726860, TriggerClass_Load_Prefix, 0x5) +{ + GET_STACK(TriggerClass*, pItem, 0x4); + GET_STACK(IStream*, pStm, 0x8); + + TriggerExt::ExtMap.PrepareStream(pItem, pStm); + + return 0; +} + +DEFINE_HOOK(0x7268CB, TriggerClass_Load_Suffix, 0x4) +{ + TriggerExt::ExtMap.LoadStatic(); + + return 0; +} + +DEFINE_HOOK(0x7268D0, TriggerClass_Save_Prefix, 0x8) +{ + GET_STACK(TriggerClass*, pItem, 0x4); + GET_STACK(IStream*, pStm, 0x8); + + TriggerExt::ExtMap.PrepareStream(pItem, pStm); + + return 0; +} + +DEFINE_HOOK(0x7268EA, TriggerClass_Save_Suffix, 0x5) +{ + TriggerExt::ExtMap.SaveStatic(); + + return 0; +} + +// ============================= +// container + +TriggerExt::ExtContainer::ExtContainer() : Container("TriggerClass") { } + +TriggerExt::ExtContainer::~ExtContainer() = default; + +// ============================= +// container hooks diff --git a/src/Ext/Trigger/Body.h b/src/Ext/Trigger/Body.h new file mode 100644 index 0000000000..1cf57b8428 --- /dev/null +++ b/src/Ext/Trigger/Body.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include + +#include + +#include +#include +#include +#include + +class TriggerExt +{ +public: + using base_type = TriggerClass; + + static constexpr DWORD Canary = 0x73171331; + + class ExtData final : public Extension + { + public: + std::vector SortedEventsList; + PhobosMap SequentialTimers; + std::map SequentialTimersOriginalValue; + PhobosMap ParallelTimers; + std::map ParallelTimersOriginalValue; + int SequentialSwitchModeIndex = -1; + + ExtData(TriggerClass* OwnerObject) : Extension(OwnerObject) + , SortedEventsList {} + , SequentialTimers {} + , SequentialTimersOriginalValue {} + , ParallelTimers {} + , ParallelTimersOriginalValue {} + , SequentialSwitchModeIndex { -1 } + { } + + virtual ~ExtData() = default; + + virtual void InvalidatePointer(void* ptr, bool bRemoved) override { } + + virtual void LoadFromStream(PhobosStreamReader& Stm) override; + virtual void SaveToStream(PhobosStreamWriter& Stm) override; + + private: + template + void Serialize(T& Stm); + }; + + class ExtContainer final : public Container + { + public: + ExtContainer(); + ~ExtContainer(); + }; + + static ExtContainer ExtMap; +}; diff --git a/src/Ext/Trigger/Hooks.cpp b/src/Ext/Trigger/Hooks.cpp index 33e85a4683..2d0724283f 100644 --- a/src/Ext/Trigger/Hooks.cpp +++ b/src/Ext/Trigger/Hooks.cpp @@ -1,8 +1,11 @@ #include +#include +#include #include #include +#include DEFINE_HOOK(0x727064, TriggerTypeClass_HasLocalSetOrClearedEvent, 0x5) { @@ -27,3 +30,214 @@ DEFINE_HOOK(0x727024, TriggerTypeClass_HasGlobalSetOrClearedEvent, 0x5) ? 0x72702E : 0x727029; } + +DEFINE_HOOK(0x72612C, TriggerClass_CTOR_ForceSequentialEvents, 0x7) +{ + GET(TriggerClass*, pThis, ESI); + + if (!pThis->Type) + return 0; + + auto pExt = TriggerExt::ExtMap.Find(pThis); + auto pCurrentEvent = pThis->Type->FirstEvent; + + while (pCurrentEvent) + { + pExt->SortedEventsList.emplace_back(pCurrentEvent); + pCurrentEvent = pCurrentEvent->NextEvent; + } + + std::reverse(pExt->SortedEventsList.begin(), pExt->SortedEventsList.end()); + + for (std::size_t i = 0; i < pExt->SortedEventsList.size(); i++) + { + pCurrentEvent = pExt->SortedEventsList[i]; + + if (static_cast(pCurrentEvent->EventKind) == PhobosTriggerEvent::ForceSequentialEvents) + { + pExt->SequentialSwitchModeIndex = i; + continue; + } + + if (pCurrentEvent->EventKind != TriggerEvent::ElapsedTime && pCurrentEvent->EventKind != TriggerEvent::RandomDelay) + continue; + + int countdown = 0; + + if (pCurrentEvent->EventKind == TriggerEvent::ElapsedTime) // Event 13 "Elapsed Time..." + countdown = pCurrentEvent->Value; + else // Event 51 "Random delay..." + countdown = ScenarioClass::Instance->Random.RandomRanged(static_cast(pCurrentEvent->Value * 0.5), static_cast(pCurrentEvent->Value * 1.5)); + + if (pExt->SequentialSwitchModeIndex >= 0) + { + pExt->SequentialTimersOriginalValue[i] = pCurrentEvent->EventKind == TriggerEvent::ElapsedTime ? pCurrentEvent->Value : pCurrentEvent->Value * -1; + pExt->SequentialTimers[i].Start(15 * countdown); + pExt->SequentialTimers[i].Pause(); + } + else + { + pExt->ParallelTimersOriginalValue[i] = pCurrentEvent->EventKind == TriggerEvent::ElapsedTime ? pCurrentEvent->Value : pCurrentEvent->Value * -1; + pExt->ParallelTimers[i].Start(15 * countdown); + } + } + + return 0; +} + +// TriggerClass::RegisterEvent(...) rewrite +DEFINE_HOOK(0x7264C0, TriggerClass_RegisterEvent_ForceSequentialEvents, 0x0) +{ + enum { SkipGameCode = 0x7265B1 }; + + GET(TriggerClass*, pThis, ECX); + GET_STACK(TriggerEvent, nEvent, 0x4); + GET_STACK(TechnoClass*, pTechno, 0x8); + GET_STACK(bool, skipStuff, 0xC); + GET_STACK(bool, isPersistant, 0x10); + GET_STACK(ObjectClass*, pPayback, 0x14); // <-- Warning! YRpp call HasOccured(...) doesn't have the last argument... + + if (!pThis->Enabled || pThis->Destroyed) + { + R->AL(false); + return SkipGameCode; + } + + auto pExt = TriggerExt::ExtMap.Find(pThis); + bool isSequentialMode = false; // Flag: Controls if short-circuit is active for subsequent events + bool allEventsSuccessful = true; + int nPredecessorEventsCompleted = 0; + + if (!skipStuff) + { + // Check status of the trigger events in sequential logic (INI order) + for (std::size_t i = 0; i < pExt->SortedEventsList.size(); i++) + { + const auto pCurrentEvent = pExt->SortedEventsList[i]; + bool alreadyOccured = pThis->HasEventOccured(i); + bool triggeredNow = false; + auto eventTimer = pThis->Timer; // Fallback + + if (pExt->ParallelTimers.contains(i)) + { + eventTimer = pExt->ParallelTimers[i]; + } + else if (pExt->SequentialTimers.contains(i)) + { + if (pExt->SequentialTimers[i].HasTimeLeft() + && !pExt->SequentialTimers[i].InProgress() + && !pExt->SequentialTimers[i].Completed()) + { + pExt->SequentialTimers[i].Resume(); + } + + eventTimer = pExt->SequentialTimers[i]; + } + + if (static_cast(pCurrentEvent->EventKind) == PhobosTriggerEvent::ForceSequentialEvents) + { + bool predecessorsCompleted = false; + + if (nPredecessorEventsCompleted >= pExt->SequentialSwitchModeIndex) + predecessorsCompleted = true; + + if (predecessorsCompleted) + { + pThis->MarkEventAsOccured(i); + alreadyOccured = true; + triggeredNow = true; + isSequentialMode = true; // Activate sequential mode for the rest of the INI events + } + else + { + allEventsSuccessful = false; + R->AL(false); + return SkipGameCode; // Short-circuit + } + } + + if (!alreadyOccured) + { + HouseClass* pEventOwner = HouseClass::FindByCountryName(pThis->Type->House->ID); + TechnoClass* pPaybackTechno = static_cast(pPayback); + + triggeredNow = pCurrentEvent->HasOccured( + static_cast(nEvent), + pEventOwner, + pTechno, + &eventTimer, + &isPersistant); + // Note: I think HasOccured in YRpp is wrong, where is the last parameter for "pPaybackTechno"? I mean this "payback1": + // TEventClass::operator()(v8, tevent, v9, &techno->r.m.o, &this->Event.Timer, &is_persistant, payback1)) ) + // In pseudocode: + // bool __thiscall TEventClass::operator()(TEventClass *this, int event, HouseClass *house, ObjectClass *obj, CDTimerClass *td, bool *bool1, TechnoClass *source) + } + + if (alreadyOccured || triggeredNow) + { + HouseClass* pNewHouse = pCurrentEvent->House; + + if (pNewHouse) + pThis->House = pNewHouse; + + if (isPersistant && pCurrentEvent->GetStateA() && pCurrentEvent->GetStateB()) + pThis->MarkEventAsOccured(i); //pThis->OccuredEvents |= eventBit; + + nPredecessorEventsCompleted++; + } + else + { + // Conditional short-circuit on Failure + allEventsSuccessful = false; + + if (isSequentialMode) + { + R->AL(false); + return SkipGameCode; + } + } + } + } + + if (allEventsSuccessful || skipStuff) + { + if (isPersistant) + { + pThis->ResetTimers(); // Is really needed now? Maybe, because YRpp is incomplete and looks that each event have its own timer inside a struct... or something similar. I'll preserve this for now that doesn't hurt having this here... + + for (std::size_t i = 0; i < pExt->ParallelTimersOriginalValue.size(); i++) + { + int timerValue = pExt->ParallelTimersOriginalValue[i]; + + if (timerValue < 0) + { + // Generate random value for event 51 "Delayed timer" + timerValue = ScenarioClass::Instance->Random.RandomRanged(static_cast(std::abs(timerValue) * 0.5), static_cast(std::abs(timerValue) * 1.5)); + } + + pExt->ParallelTimers[i].Start(15 * timerValue); + } + + for (std::size_t i = 0; i < pExt->SequentialTimersOriginalValue.size(); i++) + { + int timerValue = pExt->SequentialTimersOriginalValue[i]; + + if (timerValue < 0) + { + // Generate random value for event 51 "Delayed timer" + timerValue = ScenarioClass::Instance->Random.RandomRanged(static_cast(std::abs(timerValue) * 0.5), static_cast(std::abs(timerValue) * 1.5)); + } + + pExt->SequentialTimers[i].Start(15 * timerValue); + pExt->SequentialTimers[i].Pause(); + } + } + } + + if (allEventsSuccessful) + Debug::Log("Starting actions of the trigger: [%s] - %s\n", pThis->Type->ID, pThis->Type->Name); + + R->AL(allEventsSuccessful); + + return SkipGameCode; +}