From fb364d108e23a9995626f2d7103b658a85f0c342 Mon Sep 17 00:00:00 2001 From: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:21:43 +0200 Subject: [PATCH] Pepsi Vanilla goated asf --- Aeon-740/EventHandler.h | 312 +++++++++++++++++++++++++++++++ Aeon-740/GameModeHooks.h | 29 ++- Aeon-740/Globals.h | 7 +- Aeon-740/PlayerControllerHooks.h | 9 + Aeon-740/dllmain.cpp | 12 +- Aeon-740/framework.h | 1 + README.md | 25 +-- 7 files changed, 365 insertions(+), 30 deletions(-) create mode 100644 Aeon-740/EventHandler.h diff --git a/Aeon-740/EventHandler.h b/Aeon-740/EventHandler.h new file mode 100644 index 00000000..78ef48f8 --- /dev/null +++ b/Aeon-740/EventHandler.h @@ -0,0 +1,312 @@ +#pragma once +#include "framework.h" + +// Custom Live Event System +// +// Architecture overview: +// - Reuses AFortGameStateAthena's existing replicated timestamp fields for the +// HUD countdown. WarmupCountdownEndTime is a Net-replicated float; the client +// HUD reads (WarmupCountdownEndTime - GetTimeSeconds()) to display remaining +// seconds. The server sets this once and never needs to push tick-by-tick +// updates – replication handles the rest. +// - Uses ALevelSequenceActor + ULevelSequencePlayer (SDK) for cinematics. +// bReplicatePlayback = true means all clients mirror play/stop automatically. +// - Uses APlayerController::ClientPlaySound (reliable replicated RPC) for audio. +// - Uses APlayerController::SetIgnoreMoveInput / SetIgnoreLookInput to lock players. +// - A dedicated Win32 thread drives the timeline. During the countdown phase the +// thread polls GetTimeSeconds() every 100 ms instead of calling Sleep() for the +// full duration, so the server wakes up exactly when the timestamp is reached. + +namespace EventHandler +{ + + enum class ECustomEventPhase : uint8 + { + Idle, // No event scheduled yet + Waiting, // Grace period – players are finished their loading screens + Countdown, // HUD countdown is live; server polls until timestamp reached + EventStart, // Players teleported, movement locked + Phase1, // Opening cinematic + Phase2, // Main event sequence + Phase3, // Finale + EventEnd, // Cleanup, movement restored + }; + + static ECustomEventPhase CurrentPhase = ECustomEventPhase::Idle; + static bool bEventActive = false; + + static const string SequencePath_Phase1 = + "/Game/Apfel/Cinematics/LevelSequences/LS_CustomEvent_Phase1.LS_CustomEvent_Phase1"; + static const string SequencePath_Phase2 = + "/Game/Apfel/Cinematics/LevelSequences/LS_CustomEvent_Phase2.LS_CustomEvent_Phase2"; + static const string SequencePath_Phase3 = + "/Game/Apfel/Cinematics/LevelSequences/LS_CustomEvent_Phase3.LS_CustomEvent_Phase3"; + + static const string SoundPath_Intro = "/Game/Apfel/Audio/SC_EventIntro.SC_EventIntro"; + static const string SoundPath_Main = "/Game/Apfel/Audio/SC_EventMain.SC_EventMain"; + static const string SoundPath_Finale = "/Game/Apfel/Audio/SC_EventFinale.SC_EventFinale"; + + // World-space event staging location + static FVector EventStagingLocation = { 0.0f, 0.0f, 5000.0f }; + static FRotator EventStagingRotation = { 0.0f, 0.0f, 0.0f }; + + + // Returns the current server world time in seconds. + static float GetWorldTime() + { + return UGameplayStatics::GetDefaultObj()->GetTimeSeconds(Globals::GetWorld()); + } + + // Set the centralized countdown on AFortGameStateAthena and return the + // absolute end timestamp. WarmupCountdownStartTime / WarmupCountdownEndTime + // are Net-replicated floats; clients read them every tick to drive the HUD + // display without any additional server -> client messaging. + static float BeginCountdown(float DurationSeconds) + { + float Now = GetWorldTime(); + float EndTime = Now + DurationSeconds; + + AFortGameStateAthena* GS = Globals::GetGameState(); + if (GS) + { + GS->WarmupCountdownStartTime = Now; + GS->WarmupCountdownEndTime = EndTime; // <- clients read this for the timer + GS->bIsInCountdown = true; + } + + LogInfo("EventHandler: Countdown set. EndTime={:.1f} (in {:.0f}s)", EndTime, DurationSeconds); + return EndTime; + } + + // Block the calling thread until the world clock reaches the given timestamp. + static void WaitUntil(float Timestamp) + { + while (GetWorldTime() < Timestamp) + Sleep(100); + } + + static void EndCountdown() + { + AFortGameStateAthena* GS = Globals::GetGameState(); + if (GS) + GS->bIsInCountdown = false; + } + + static void SetControllerLocked(APlayerController* PC, bool bLocked) + { + if (!PC) return; + if (bLocked) + { + PC->SetIgnoreMoveInput(true); + PC->SetIgnoreLookInput(true); + } + else + { + PC->ResetIgnoreMoveInput(); + PC->ResetIgnoreLookInput(); + } + } + + static void SetAllPlayersLocked(bool bLocked) + { + UWorld* World = Globals::GetWorld(); + if (!World || !World->GameState) return; + + TArray& Players = World->GameState->PlayerArray; + for (int32 i = 0; i < Players.Num(); i++) + { + if (!Players[i]) continue; + APlayerController* PC = static_cast(Players[i]->GetOwner()); + SetControllerLocked(PC, bLocked); + } + } + + static void TeleportAllToStagingArea() + { + UWorld* World = Globals::GetWorld(); + if (!World || !World->GameState) return; + + TArray& Players = World->GameState->PlayerArray; + for (int32 i = 0; i < Players.Num(); i++) + { + if (!Players[i]) continue; + + APlayerController* PC = static_cast(Players[i]->GetOwner()); + if (!PC) continue; + + APawn* Pawn = PC->Pawn; + if (Pawn) + { + FHitResult Hit; + Pawn->K2_SetActorLocation(EventStagingLocation, false, &Hit, true); + } + + PC->ClientSetLocation(EventStagingLocation, EventStagingRotation); + } + + LogInfo("EventHandler: Teleported {} players to staging area.", Players.Num()); + } + + static void BroadcastSound(const string& SoundPath) + { + USoundBase* Sound = SDKUtils::FindObject(SoundPath); + if (!Sound) + Sound = SDKUtils::LoadObject(SoundPath); + + if (!Sound) + { + LogInfo("EventHandler: Sound asset not found ({})", SoundPath); + return; + } + + UWorld* World = Globals::GetWorld(); + if (!World || !World->GameState) return; + + TArray& Players = World->GameState->PlayerArray; + for (int32 i = 0; i < Players.Num(); i++) + { + if (!Players[i]) continue; + APlayerController* PC = static_cast(Players[i]->GetOwner()); + if (!PC) continue; + + PC->ClientPlaySound(Sound, 1.0f, 1.0f); + } + + LogInfo("EventHandler: Broadcast sound to {} players.", Players.Num()); + } + + static ALevelSequenceActor* PlaySequence(const string& SequencePath) + { + ULevelSequence* Sequence = SDKUtils::FindObject(SequencePath); + if (!Sequence) + Sequence = SDKUtils::LoadObject(SequencePath); + + if (!Sequence) + { + LogInfo("EventHandler: LevelSequence not found ({})", SequencePath); + return nullptr; + } + + FMovieSceneSequencePlaybackSettings Settings{}; + Settings.PlayRate = 1.0f; + Settings.LoopCount.Value = 0; // play once; set to -1 for infinite loop + Settings.StartTime = 0.0f; + + ALevelSequenceActor* OutActor = nullptr; + ULevelSequencePlayer* Player = ULevelSequencePlayer::CreateLevelSequencePlayer( + Globals::GetWorld(), + Sequence, + Settings, + &OutActor + ); + + if (OutActor) + { + OutActor->bReplicatePlayback = true; + OutActor->SetReplicatePlayback(true); + } + + if (Player) + Player->Play(); + + LogInfo("EventHandler: Started sequence: {}", SequencePath); + return OutActor; + } + + static void StopSequence(ALevelSequenceActor*& OutActor) + { + if (!OutActor) return; + + if (OutActor->SequencePlayer) + OutActor->SequencePlayer->Stop(); + + OutActor->K2_DestroyActor(); + OutActor = nullptr; + } + + DWORD WINAPI EventTimeline(LPVOID) + { + LogInfo("EventHandler: Timeline thread started."); + + // Phase: Waiting – 10 s grace period for loading screens to clear + CurrentPhase = ECustomEventPhase::Waiting; + LogInfo("EventHandler: [Waiting] 10 s grace period..."); + Sleep(10000); + + // Phase: Countdown + // BeginCountdown() writes WarmupCountdownEndTime onto the GameState. + // That field is Net-replicated; clients poll it each tick to drive the + // HUD timer without any extra server messaging. + // WaitUntil() blocks this thread until the timestamp is reached. + CurrentPhase = ECustomEventPhase::Countdown; + LogInfo("EventHandler: [Countdown] Starting 30 s timestamp countdown..."); + float EventStartTimestamp = BeginCountdown(30.0f); + WaitUntil(EventStartTimestamp); // server advances exactly when timer hits 0 + EndCountdown(); + + // Phase: Event Start + CurrentPhase = ECustomEventPhase::EventStart; + bEventActive = true; + LogInfo("EventHandler: [EventStart] Teleporting and locking players."); + + TeleportAllToStagingArea(); + Sleep(2000); // brief settle time after teleport RPC propagates + SetAllPlayersLocked(true); + + // Phase 1 – Opening cinematic (15 s) + CurrentPhase = ECustomEventPhase::Phase1; + LogInfo("EventHandler: [Phase1] Opening cinematic."); + + ALevelSequenceActor* SeqActor = PlaySequence(SequencePath_Phase1); + BroadcastSound(SoundPath_Intro); + Sleep(15000); + StopSequence(SeqActor); + + // Phase 2 – Main event sequence (30 s) + CurrentPhase = ECustomEventPhase::Phase2; + LogInfo("EventHandler: [Phase2] Main event sequence."); + + SeqActor = PlaySequence(SequencePath_Phase2); + BroadcastSound(SoundPath_Main); + Sleep(30000); + StopSequence(SeqActor); + + // Phase 3 – Finale (20 s) + CurrentPhase = ECustomEventPhase::Phase3; + LogInfo("EventHandler: [Phase3] Finale."); + + SeqActor = PlaySequence(SequencePath_Phase3); + BroadcastSound(SoundPath_Finale); + Sleep(20000); + StopSequence(SeqActor); + + // Phase: Event End – restore players, clear countdown state + CurrentPhase = ECustomEventPhase::EventEnd; + LogInfo("EventHandler: [EventEnd] Restoring players."); + + SetAllPlayersLocked(false); + + AFortGameStateAthena* GS = Globals::GetGameState(); + if (GS) + { + GS->bIsInCountdown = false; + GS->bIsInFinalCountdown = false; + } + + LogInfo("EventHandler: Event complete."); + return 0; + } + + + static void BeginEvent() + { + if (bEventActive || CurrentPhase != ECustomEventPhase::Idle) + { + LogInfo("EventHandler::BeginEvent: Already running or completed, ignoring."); + return; + } + + LogInfo("EventHandler: Scheduling live event timeline..."); + CreateThread(nullptr, 0, EventTimeline, nullptr, 0, nullptr); + } +} diff --git a/Aeon-740/GameModeHooks.h b/Aeon-740/GameModeHooks.h index c66eb19d..c14651e3 100644 --- a/Aeon-740/GameModeHooks.h +++ b/Aeon-740/GameModeHooks.h @@ -30,13 +30,36 @@ namespace Hooks } } + // The custom event map may not contain an AFortAthenaMapInfo actor because + // of skids. + // Guard against returning early so the listen server still comes up. if (!GameState->MapInfo) - return false; + { + LogInfo("GameMode::ReadyToStartMatch: No MapInfo (custom event map) -> continuing without it."); + + GameMode->DefaultPawnClass = SDK::APlayerPawn_Athena_C::StaticClass(); + + if (!Globals::bListening) + { + ServerHandler::Listen(); + Hooks::Actor::Initialize(); + LootHandler::Initialize(); + + GameMode->GameSession->MaxPlayers = 100; + GameMode->WarmupRequiredPlayerCount = 1; + + GameState->OnRep_CurrentPlaylistInfo(); + } + + GameMode->bWorldIsReady = true; + return true; // Signal that the match may start without MapInfo + } GameMode->DefaultPawnClass = SDK::APlayerPawn_Athena_C::StaticClass(); + // Showfoundation already guards against foundations not existing. GameModeHandler::ShowFoundation(SDKUtils::FindObject("/Game/Athena/Maps/Athena_POI_Foundations.Athena_POI_Foundations.PersistentLevel.LF_Athena_POI_25x36")); - GameModeHandler::ShowFoundation(SDKUtils::FindObject("/Game/Athena/Maps/Athena_POI_Foundations.Athena_POI_Foundations.PersistentLevel.ShopsNew")); + GameModeHandler::ShowFoundation(SDKUtils::FindObject("/Game/Athena/Maps/Athena_POI_Foundations.Athena_POI_Foundations.PersistentLevel.ShopsNew")); if (!Globals::bListening) { @@ -72,4 +95,4 @@ namespace Hooks HookingManager::CreateMinHook(Memory::GetAddress(GOffsets::GameMode::SpawnDefaultPawnFor), hkSpawnDefaultPawnFor); } } -} \ No newline at end of file +} diff --git a/Aeon-740/Globals.h b/Aeon-740/Globals.h index a50aee1a..6739f325 100644 --- a/Aeon-740/Globals.h +++ b/Aeon-740/Globals.h @@ -3,9 +3,10 @@ namespace Globals { - static bool bPlaylistSetup = false; - static bool bListening = false; - static bool bMcp = false; + static bool bPlaylistSetup = false; + static bool bListening = false; + static bool bMcp = false; + static bool bEventScheduled = false; // Guards EventHandler::BeginEvent() against duplicate calls UWorld* GetWorld() { diff --git a/Aeon-740/PlayerControllerHooks.h b/Aeon-740/PlayerControllerHooks.h index b2d42bda..56282b2c 100644 --- a/Aeon-740/PlayerControllerHooks.h +++ b/Aeon-740/PlayerControllerHooks.h @@ -74,6 +74,15 @@ namespace Hooks InventoryHandler::InitializePlayer(PlayerController); LootHandler::SpawnFloorLoot(); + // Schedule the live event timeline on the first player to drop their + // loading screen. EventHandler::BeginEvent() is idempotent – calling it + // more than once (for additional players) is safe and has no effect. + if (!Globals::bEventScheduled) + { + Globals::bEventScheduled = true; + EventHandler::BeginEvent(); + } + return Defines::PlayerController::ServerLoadingScreenDropped(PlayerController); } diff --git a/Aeon-740/dllmain.cpp b/Aeon-740/dllmain.cpp index 16eaa99e..71a10c20 100644 --- a/Aeon-740/dllmain.cpp +++ b/Aeon-740/dllmain.cpp @@ -1,5 +1,8 @@ #include "framework.h" +// asset path for the custom event map. +static const wchar_t* CustomMapPath = L"/Game/Apfel/Maps/CustomEvent"; + DWORD InputThread(LPVOID) { while (true) @@ -8,6 +11,12 @@ DWORD InputThread(LPVOID) { UKismetSystemLibrary::ExecuteConsoleCommand(UWorld::GetWorld(), L"startaircraft", nullptr); } + + // F7 – manually force-start the live event + if (GetAsyncKeyState(VK_F7) & 0x01) + { + EventHandler::BeginEvent(); + } } } @@ -22,7 +31,7 @@ DWORD Initialize(LPVOID) Sleep(5000); TArray& LocalPlayers = UWorld::GetWorld()->OwningGameInstance->LocalPlayers; - LocalPlayers[0]->PlayerController->SwitchLevel(L"Athena_Terrain"); + LocalPlayers[0]->PlayerController->SwitchLevel(CustomMapPath); LocalPlayers.Remove(0); LocalPlayers.Free(); @@ -55,4 +64,3 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReason, LPVOID lpReserved) } return TRUE; } - diff --git a/Aeon-740/framework.h b/Aeon-740/framework.h index a1b412f1..2772dc51 100644 --- a/Aeon-740/framework.h +++ b/Aeon-740/framework.h @@ -28,6 +28,7 @@ using namespace std; #include "InventoryHandler.h" #include "LootHandler.h" #include "ServerHandler.h" +#include "EventHandler.h" #include "AbilitiesHooks.h" #include "ActorHooks.h" diff --git a/README.md b/README.md index b8d3af20..85192840 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,6 @@ -# Aeon-7.40 +# UnrealEvent -**Fortnite 7.40 GameServer** +**Fortnite 7.40 Custom Live Event POC**

-This is just a simple gameserver for 7.40 which is being made and will receive occasional updates until it's playable, maybe near 1:1. -
-
-**NAMESPACES AND FILES** -
-The namespaces, files and layout are different than what you may normally see, this is so it is easy to read and navigate. -
-**Handlers:** Handles everything efficiently. -
-**Managers:** Manages everything easily. -
-**Hooks:** Contains all the Hooks of functions. -
-
-This isn't planned on being used at all, you may use it and port it to other versions if you'd like, **with credit**. -
-There will be no support on how to set this up and use it as it should be pretty clear. -
-
-*This is just so I could expand my knowledge and provide, this could also be used to understand how servers work.* +This is just a simple gameserver for 7.40, based on nax1800's Aeon, it adds custom live event logic, the pak project for this is still being polished, but a person that understands UE can defo use this. \ No newline at end of file