Pepsi Vanilla goated asf

This commit is contained in:
ApfelTeeSaft
2026-04-22 07:21:43 +02:00
parent ea0a81d5f4
commit fb364d108e
7 changed files with 365 additions and 30 deletions
+312
View File
@@ -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<APlayerState*>& Players = World->GameState->PlayerArray;
for (int32 i = 0; i < Players.Num(); i++)
{
if (!Players[i]) continue;
APlayerController* PC = static_cast<APlayerController*>(Players[i]->GetOwner());
SetControllerLocked(PC, bLocked);
}
}
static void TeleportAllToStagingArea()
{
UWorld* World = Globals::GetWorld();
if (!World || !World->GameState) return;
TArray<APlayerState*>& Players = World->GameState->PlayerArray;
for (int32 i = 0; i < Players.Num(); i++)
{
if (!Players[i]) continue;
APlayerController* PC = static_cast<APlayerController*>(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<USoundBase>(SoundPath);
if (!Sound)
Sound = SDKUtils::LoadObject<USoundBase>(SoundPath);
if (!Sound)
{
LogInfo("EventHandler: Sound asset not found ({})", SoundPath);
return;
}
UWorld* World = Globals::GetWorld();
if (!World || !World->GameState) return;
TArray<APlayerState*>& Players = World->GameState->PlayerArray;
for (int32 i = 0; i < Players.Num(); i++)
{
if (!Players[i]) continue;
APlayerController* PC = static_cast<APlayerController*>(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<ULevelSequence>(SequencePath);
if (!Sequence)
Sequence = SDKUtils::LoadObject<ULevelSequence>(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);
}
}
+26 -3
View File
@@ -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<ABuildingFoundation>("/Game/Athena/Maps/Athena_POI_Foundations.Athena_POI_Foundations.PersistentLevel.LF_Athena_POI_25x36"));
GameModeHandler::ShowFoundation(SDKUtils::FindObject<ABuildingFoundation>("/Game/Athena/Maps/Athena_POI_Foundations.Athena_POI_Foundations.PersistentLevel.ShopsNew"));
GameModeHandler::ShowFoundation(SDKUtils::FindObject<ABuildingFoundation>("/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);
}
}
}
}
+4 -3
View File
@@ -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()
{
+9
View File
@@ -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);
}
+10 -2
View File
@@ -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<ULocalPlayer*>& 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;
}
+1
View File
@@ -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"
+3 -22
View File
@@ -1,25 +1,6 @@
# Aeon-7.40
# UnrealEvent
**Fortnite 7.40 GameServer**
**Fortnite 7.40 Custom Live Event POC**
<br/>
<br/>
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.
<br/>
<br/>
**NAMESPACES AND FILES**
<br/>
The namespaces, files and layout are different than what you may normally see, this is so it is easy to read and navigate.
<br/>
**Handlers:** Handles everything efficiently.
<br/>
**Managers:** Manages everything easily.
<br/>
**Hooks:** Contains all the Hooks of functions.
<br/>
<br/>
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**.
<br/>
There will be no support on how to set this up and use it as it should be pretty clear.
<br/>
<br/>
*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.