Added client-side Thinkers

Adds support for client-side Thinkers, Actors, and ACS scripts (ACS uses the existing CLIENTSIDE keyword). These will tick regardless of the network state allowing for localized client handling and are put in their own separate lists so they can't be accidentally accessed by server code. They currently aren't serialized since this would have no meaning for other clients in the game that would get saved. Other logic like the menu, console, HUD, and particles have also been moved to client-side ticking to prevent them from becoming locked up by poor network conditions. Additionally, screenshotting and the automap are now handled immediately instead of having to wait for any game tick to run first, making them free of net lag.
This commit is contained in:
Boondorl 2024-11-09 22:18:04 -05:00 committed by Ricardo Luís Vaz Silva
parent e4081df0db
commit f7e62a8cd6
28 changed files with 506 additions and 214 deletions

View file

@ -261,7 +261,7 @@ CCMD(togglemap)
{
if (gameaction == ga_nothing)
{
gameaction = ga_togglemap;
AM_ToggleMap();
}
}

View file

@ -573,6 +573,7 @@ void DObject::Serialize(FSerializer &arc)
SerializeFlag("justspawned", OF_JustSpawned);
SerializeFlag("spawned", OF_Spawned);
SerializeFlag("networked", OF_Networked);
SerializeFlag("clientside", OF_ClientSide);
ObjectFlags |= OF_SerialSuccess;
@ -668,7 +669,7 @@ void NetworkEntityManager::SetClientNetworkEntity(DObject* mo, const unsigned in
void NetworkEntityManager::AddNetworkEntity(DObject* const ent)
{
if (ent->IsNetworked())
if (ent->IsNetworked() || ent->IsClientside())
return;
// Slot 0 is reserved for the world.
@ -758,6 +759,18 @@ DEFINE_ACTION_FUNCTION_NATIVE(DObject, GetNetworkID, GetNetworkID)
ACTION_RETURN_INT(self->GetNetworkID());
}
static int IsClientside(DObject* self)
{
return self->IsClientside();
}
DEFINE_ACTION_FUNCTION_NATIVE(DObject, IsClientside, IsClientside)
{
PARAM_SELF_PROLOGUE(DObject);
ACTION_RETURN_BOOL(self->IsClientside());
}
static void EnableNetworking(DObject* const self, const bool enable)
{
self->EnableNetworking(enable);

View file

@ -359,6 +359,7 @@ private:
public:
inline bool IsNetworked() const { return (ObjectFlags & OF_Networked); }
inline uint32_t GetNetworkID() const { return _networkID; }
inline bool IsClientside() const { return (ObjectFlags & OF_ClientSide); }
void SetNetworkID(const uint32_t id);
void ClearNetworkID();
void RemoveFromNetwork();

View file

@ -27,6 +27,7 @@ enum EObjectFlags
OF_Spawned = 1 << 12, // Thinker was spawned at all (some thinkers get deleted before spawning)
OF_Released = 1 << 13, // Object was released from the GC system and should not be processed by GC function
OF_Networked = 1 << 14, // Object has a unique network identifier that makes it synchronizable between all clients.
OF_ClientSide = 1 << 15, // Object is owned by a specific client rather than the server
};
template<class T> class TObjPtr;

View file

@ -440,7 +440,7 @@ DObject *PClass::CreateNew()
ConstructNative (mem);
if (Defaults != nullptr)
((DObject *)mem)->ObjectFlags |= ((DObject *)Defaults)->ObjectFlags & OF_Transient;
((DObject *)mem)->ObjectFlags |= ((DObject *)Defaults)->ObjectFlags & (OF_Transient | OF_ClientSide);
((DObject *)mem)->SetClass (const_cast<PClass *>(this));
InitializeSpecials(mem, Defaults, &PClass::SpecialInits);

View file

@ -73,6 +73,8 @@
#include "i_interface.h"
#include "savegamemanager.h"
void P_RunClientsideLogic();
EXTERN_CVAR (Int, disableautosave)
EXTERN_CVAR (Int, autosavecount)
EXTERN_CVAR (Bool, cl_capfps)
@ -1800,6 +1802,12 @@ static bool ShouldStabilizeTick()
void TryRunTics()
{
GC::CheckGC();
if (ToggleFullscreen)
{
ToggleFullscreen = false;
AddCommandString("toggle vid_fullscreen");
}
bool doWait = (cl_capfps || pauseext || (!netgame && r_NoInterpolate && !M_IsAnimated()));
if (vid_dontdowait && (vid_maxfps > 0 || vid_vsync))
@ -1852,14 +1860,6 @@ void TryRunTics()
// commands to predict.
if (runTics <= 0)
{
// If we actually did have some tics available, make sure the UI
// still has a chance to run.
for (int i = 0; i < totalTics; ++i)
{
C_Ticker();
M_Ticker();
}
// If we're in between a tic, try and balance things out.
if (totalTics <= 0)
TicStabilityWait();
@ -1875,6 +1875,11 @@ void TryRunTics()
S_UpdateSounds(players[consoleplayer].camera); // Update sounds only after predicting the client's newest position.
}
// If we actually did have some tics available, make sure the UI
// still has a chance to run.
for (int i = 0; i < totalTics; ++i)
P_RunClientsideLogic();
return;
}
@ -1895,8 +1900,6 @@ void TryRunTics()
if (advancedemo)
D_DoAdvanceDemo();
C_Ticker();
M_Ticker();
G_Ticker();
MakeConsistencies();
++gametic;
@ -1912,6 +1915,11 @@ void TryRunTics()
}
P_PredictPlayer(&players[consoleplayer]);
S_UpdateSounds(players[consoleplayer].camera); // Update sounds only after predicting the client's newest position.
// These should use the actual tics since they're not actually tied to the gameplay logic.
// Make sure it always comes after so the HUD has the correct game state when updating.
for (int i = 0; i < totalTics; ++i)
P_RunClientsideLogic();
}
void Net_NewClientTic()

View file

@ -1136,12 +1136,6 @@ void G_Ticker ()
primaryLevel->DoReborn(i, false);
}
if (ToggleFullscreen)
{
ToggleFullscreen = false;
AddCommandString ("toggle vid_fullscreen");
}
// do things to change the game state
oldgamestate = gamestate;
while (gameaction != ga_nothing)
@ -1192,19 +1186,10 @@ void G_Ticker ()
case ga_worlddone:
G_DoWorldDone ();
break;
case ga_screenshot:
M_ScreenShot (shotfile.GetChars());
shotfile = "";
gameaction = ga_nothing;
break;
case ga_fullconsole:
G_FullConsole ();
gameaction = ga_nothing;
break;
case ga_togglemap:
AM_ToggleMap ();
gameaction = ga_nothing;
break;
case ga_resumeconversation:
P_ResumeConversation ();
gameaction = ga_nothing;
@ -1263,18 +1248,12 @@ void G_Ticker ()
}
}
// [ZZ] also tick the UI part of the events
primaryLevel->localEventManager->UiTick();
C_RunDelayedCommands();
// do main actions
switch (gamestate)
{
case GS_LEVEL:
P_Ticker ();
primaryLevel->automap->Ticker ();
break;
case GS_TITLELEVEL:
P_Ticker ();
break;
@ -1303,9 +1282,6 @@ void G_Ticker ()
default:
break;
}
// [MK] Additional ticker for UI events right after all others
primaryLevel->localEventManager->PostUiTick();
}
@ -1844,7 +1820,8 @@ void G_ScreenShot (const char *filename)
if (gameaction == ga_nothing)
{
shotfile = filename;
gameaction = ga_screenshot;
M_ScreenShot(shotfile.GetChars());
shotfile = "";
}
}

View file

@ -457,7 +457,7 @@ void G_NewInit ()
int i;
// Destory all old player refrences that may still exist
TThinkerIterator<AActor> it(primaryLevel, NAME_PlayerPawn, STAT_TRAVELLING);
TThinkerIterator<AActor> it(primaryLevel, NAME_PlayerPawn, STAT_TRAVELLING, false);
AActor *pawn, *next;
next = it.Next();
@ -473,6 +473,7 @@ void G_NewInit ()
// Destroy thinkers that may remain after change level failure
// Usually, the list contains just a sentinel when such error occurred
primaryLevel->Thinkers.DestroyThinkersInList(STAT_TRAVELLING);
primaryLevel->ClientsideThinkers.DestroyThinkersInList(STAT_TRAVELLING); // This isn't currently supported, but maybe in the future
G_ClearSnapshots ();
netgame = false;
@ -586,6 +587,7 @@ void G_InitNew (const char *mapname, bool bTitleLevel)
for (auto Level : AllLevels())
{
Level->Thinkers.DestroyThinkersInList(STAT_STATIC);
Level->ClientsideThinkers.DestroyThinkersInList(STAT_STATIC);
}
if (paused)
@ -1780,6 +1782,7 @@ int FLevelLocals::FinishTravel ()
// Since this list is excluded from regular thinker cleaning, anything that may survive through here
// will endlessly multiply and severely break the following savegames or just simply crash on broken pointers.
Thinkers.DestroyThinkersInList(STAT_TRAVELLING);
ClientsideThinkers.DestroyThinkersInList(STAT_TRAVELLING);
return failnum;
}
@ -2284,6 +2287,7 @@ void FLevelLocals::Mark()
GC::Mark(SpotState);
GC::Mark(FraggleScriptThinker);
GC::Mark(ACSThinker);
GC::Mark(ClientSideACSThinker);
GC::Mark(automap);
GC::Mark(interpolator.Head);
GC::Mark(SequenceListHead);
@ -2296,6 +2300,7 @@ void FLevelLocals::Mark()
GC::Mark(localEventManager->LastEventHandler);
}
Thinkers.MarkRoots();
ClientsideThinkers.MarkRoots();
canvasTextureInfo.Mark();
for (auto &c : CorpseQueue)
{

View file

@ -138,8 +138,8 @@ struct FLevelLocals
void ClearAllSubsectorLinks();
void TranslateLineDef (line_t *ld, maplinedef_t *mld, int lineindexforid = -1);
int TranslateSectorSpecial(int special);
bool IsTIDUsed(int tid);
int FindUniqueTID(int start_tid, int limit);
bool IsTIDUsed(int tid, bool clientside);
int FindUniqueTID(int start_tid, int limit, bool clientside);
int GetConversation(int conv_id);
int GetConversation(FName classname);
void SetConversation(int convid, PClassActor *Class, int dlgindex);
@ -249,6 +249,7 @@ public:
void SpawnExtraPlayers();
void Serialize(FSerializer &arc, bool hubload);
DThinker *FirstThinker (int statnum);
DThinker* FirstClientsideThinker(int statnum);
// g_Game
void PlayerReborn (int player);
@ -295,12 +296,21 @@ public:
}
template<class T> TThinkerIterator<T> GetThinkerIterator(FName subtype = NAME_None, int statnum = MAX_STATNUM+1)
{
if (subtype == NAME_None) return TThinkerIterator<T>(this, statnum);
else return TThinkerIterator<T>(this, subtype, statnum);
if (subtype == NAME_None) return TThinkerIterator<T>(this, statnum, false);
else return TThinkerIterator<T>(this, subtype, statnum, false);
}
template<class T> TThinkerIterator<T> GetThinkerIterator(FName subtype, int statnum, AActor *prev)
{
return TThinkerIterator<T>(this, subtype, statnum, prev);
return TThinkerIterator<T>(this, subtype, statnum, prev, false);
}
template<class T> TThinkerIterator<T> GetClientsideThinkerIterator(FName subtype = NAME_None, int statnum = MAX_STATNUM + 1)
{
if (subtype == NAME_None) return TThinkerIterator<T>(this, statnum, true);
else return TThinkerIterator<T>(this, subtype, statnum, true);
}
template<class T> TThinkerIterator<T> GetClientsideThinkerIterator(FName subtype, int statnum, AActor* prev)
{
return TThinkerIterator<T>(this, subtype, statnum, prev, true);
}
FActorIterator GetActorIterator(int tid)
{
@ -314,6 +324,18 @@ public:
{
return NActorIterator(TIDHash, type, tid);
}
FActorIterator GetClientSideActorIterator(int tid)
{
return FActorIterator(ClientSideTIDHash, tid);
}
FActorIterator GetClientSideActorIterator(int tid, AActor* start)
{
return FActorIterator(ClientSideTIDHash, tid, start);
}
NActorIterator GetClientSideActorIterator(FName type, int tid)
{
return NActorIterator(ClientSideTIDHash, type, tid);
}
AActor *SingleActorFromTID(int tid, AActor *defactor)
{
return tid == 0 ? defactor : GetActorIterator(tid).Next();
@ -411,6 +433,7 @@ public:
void ClearTIDHashes ()
{
memset(TIDHash, 0, sizeof(TIDHash));
memset(ClientSideTIDHash, 0, sizeof(ClientSideTIDHash));
}
@ -441,6 +464,24 @@ public:
thinker->Construct(std::forward<Args>(args)...);
return thinker;
}
DThinker* CreateClientsideThinker(PClass* cls, int statnum = STAT_DEFAULT)
{
DThinker* thinker = static_cast<DThinker*>(cls->CreateNew());
assert(thinker->IsKindOf(RUNTIME_CLASS(DThinker)));
thinker->ObjectFlags |= OF_JustSpawned | OF_ClientSide | OF_Transient;
ClientsideThinkers.Link(thinker, statnum);
thinker->Level = this;
return thinker;
}
template<typename T, typename... Args>
T* CreateClientsideThinker(Args&&... args)
{
auto thinker = static_cast<T*>(CreateClientsideThinker(RUNTIME_CLASS(T), T::DEFAULT_STAT));
thinker->Construct(std::forward<Args>(args)...);
return thinker;
}
void SetMusic();
@ -511,6 +552,7 @@ public:
FBehaviorContainer Behaviors;
AActor *TIDHash[128];
AActor* ClientSideTIDHash[128];
TArray<FStrifeDialogueNode *> StrifeDialogues;
FDialogueIDMap DialogueRoots;
@ -674,6 +716,7 @@ public:
TArray<particle_t> Particles;
TArray<uint16_t> ParticlesInSubsec;
FThinkerCollection Thinkers;
FThinkerCollection ClientsideThinkers;
TArray<DVector2> Scrolls; // NULL if no DScrollers in this level
@ -710,6 +753,7 @@ public:
TArray<TObjPtr<AActor *>> CorpseQueue;
TObjPtr<DFraggleThinker *> FraggleScriptThinker = MakeObjPtr<DFraggleThinker*>(nullptr);
TObjPtr<DACSThinker*> ACSThinker = MakeObjPtr<DACSThinker*>(nullptr);
TObjPtr<DACSThinker*> ClientSideACSThinker = MakeObjPtr<DACSThinker*>(nullptr);
TObjPtr<DSpotState *> SpotState = MakeObjPtr<DSpotState*>(nullptr);

View file

@ -63,6 +63,7 @@ extern void ClearStrifeTypes();
TArray<PClassActor *> PClassActor::AllActorClasses;
FRandom FState::pr_statetics("StateTics");
FCRandom FState::pr_csstatetics("ClientsideStateTics");
cycle_t ActionCycles;
@ -489,7 +490,7 @@ void PClassActor::InitializeDefaults()
memset(Defaults + ParentClass->Size, 0, Size - ParentClass->Size);
}
optr->ObjectFlags = ((DObject*)ParentClass->Defaults)->ObjectFlags & OF_Transient;
optr->ObjectFlags = ((DObject*)ParentClass->Defaults)->ObjectFlags & (OF_Transient | OF_ClientSide);
}
else
{

View file

@ -150,6 +150,14 @@ public:
}
return Tics + pr_statetics.GenRand32() % (TicRange + 1);
}
inline int GetClientsideTics() const
{
if (TicRange == 0)
{
return Tics;
}
return Tics + pr_csstatetics.GenRand32() % (TicRange + 1);
}
inline int GetMisc1() const
{
return Misc1;
@ -176,6 +184,7 @@ public:
static PClassActor *StaticFindStateOwner (const FState *state, PClassActor *info);
static FString StaticGetStateName(const FState *state, PClassActor *info = nullptr);
static FRandom pr_statetics;
static FCRandom pr_csstatetics;
};

View file

@ -949,6 +949,7 @@ void FLevelLocals::Serialize(FSerializer &arc, bool hubload)
if (arc.isReading())
{
Thinkers.DestroyAllThinkers();
ClientsideThinkers.DestroyAllThinkers();
interpolator.ClearInterpolations();
arc.ReadObjects(hubload);
// If there have been object deserialization errors we must absolutely not continue here because scripted objects can do unpredictable things.

View file

@ -296,6 +296,7 @@ void FLevelLocals::ClearLevelData(bool fullgc)
interpolator.ClearInterpolations(); // [RH] Nothing to interpolate on a fresh level.
Thinkers.DestroyAllThinkers(fullgc);
ClientsideThinkers.DestroyAllThinkers(fullgc);
ClearAllSubsectorLinks(); // can't be done as part of the polyobj deletion process.
total_monsters = total_items = total_secrets =
@ -331,6 +332,7 @@ void FLevelLocals::ClearLevelData(bool fullgc)
if (SpotState) SpotState->Destroy();
SpotState = nullptr;
ACSThinker = nullptr;
ClientSideACSThinker = nullptr;
FraggleScriptThinker = nullptr;
CorpseQueue.Clear();
canvasTextureInfo.EmptyList();
@ -644,6 +646,7 @@ void P_Shutdown ()
for (auto Level : AllLevels())
{
Level->Thinkers.DestroyThinkersInList(STAT_STATIC);
Level->ClientsideThinkers.DestroyThinkersInList(STAT_STATIC);
}
P_FreeLevelData ();
// [ZZ] delete global event handlers

View file

@ -38,11 +38,61 @@
#include "events.h"
#include "actorinlines.h"
#include "g_game.h"
#include "am_map.h"
#include "i_interface.h"
extern gamestate_t wipegamestate;
extern uint8_t globalfreeze, globalchangefreeze;
void C_Ticker();
void M_Ticker();
//==========================================================================
//
// P_RunClientsideLogic
//
// Handles all logic that should be ran every tick including while
// predicting. Only put non-playsim behaviors in here to avoid desyncs
// when playing online.
//
//==========================================================================
void P_RunClientsideLogic()
{
C_Ticker();
M_Ticker();
// [ZZ] also tick the UI part of the events
primaryLevel->localEventManager->UiTick();
if (gamestate == GS_LEVEL || gamestate == GS_TITLELEVEL)
{
for (auto level : AllLevels())
{
auto it = level->GetClientsideThinkerIterator<AActor>();
AActor* ac = nullptr;
while ((ac = it.Next()) != nullptr)
{
ac->ClearInterpolation();
ac->ClearFOVInterpolation();
}
P_ThinkParticles(level); // [RH] make the particles think
level->ClientsideThinkers.RunClientsideThinkers(level);
}
StatusBar->CallTick();
// TODO: Should this be called on all maps...?
if (gamestate == GS_LEVEL)
primaryLevel->automap->Ticker();
}
// [MK] Additional ticker for UI events right after all others
primaryLevel->localEventManager->PostUiTick();
}
//==========================================================================
//
// P_CheckTickerPaused
@ -196,8 +246,6 @@ void P_Ticker (void)
ac->ClearFOVInterpolation();
}
P_ThinkParticles(Level); // [RH] make the particles think
for (i = 0; i < MAXPLAYERS; i++)
if (Level->PlayerInGame(i))
P_PlayerThink(Level->Players[i]);
@ -222,5 +270,4 @@ void P_Ticker (void)
if (players[consoleplayer].mo->Vel.Length() > primaryLevel->max_velocity) { primaryLevel->max_velocity = players[consoleplayer].mo->Vel.Length(); }
primaryLevel->avg_velocity += (players[consoleplayer].mo->Vel.Length() - primaryLevel->avg_velocity) / primaryLevel->maptime;
}
StatusBar->CallTick(); // Status bar should tick AFTER the thinkers to properly reflect the level's state at this time.
}

View file

@ -267,6 +267,66 @@ void FThinkerCollection::RunThinkers(FLevelLocals *Level)
ThinkCycles.Unclock();
}
//==========================================================================
//
// This version doesn't modify the level since that's already been done by
// the networked ticking. This also runs while the player is predicting
// to make sure it keeps ticking regardless of network game status.
//
//==========================================================================
void FThinkerCollection::RunClientsideThinkers(FLevelLocals* Level)
{
int i, count;
bool dolights;
if ((gl_lights && vid_rendermode == 4) || (r_dynlights && vid_rendermode != 4))
{
dolights = true;// Level->lights || (Level->flags3 & LEVEL3_LIGHTCREATED);
}
else
{
dolights = false;
}
auto recreateLights = [=]() {
auto it = Level->GetClientsideThinkerIterator<AActor>();
// Set dynamic lights at the end of the tick, so that this catches all changes being made through the last frame.
while (auto ac = it.Next())
{
if (ac->flags8 & MF8_RECREATELIGHTS)
{
ac->flags8 &= ~MF8_RECREATELIGHTS;
if (dolights) ac->SetDynamicLights();
}
// This was merged from P_RunEffects to eliminate the costly duplicate ThinkerIterator loop.
if ((ac->effects || ac->fountaincolor) && ac->ShouldRenderLocally() && !Level->isFrozen())
{
P_RunEffect(ac, ac->effects);
}
}
};
// Tick every thinker left from last time
for (i = STAT_FIRST_THINKING; i <= MAX_STATNUM; ++i)
{
Thinkers[i].TickThinkers(nullptr);
}
// Keep ticking the fresh thinkers until there are no new ones.
do
{
count = 0;
for (i = STAT_FIRST_THINKING; i <= MAX_STATNUM; ++i)
{
count += FreshThinkers[i].TickThinkers(&Thinkers[i]);
}
} while (count != 0);
recreateLights();
}
//==========================================================================
//
// Destroy every thinker
@ -784,6 +844,11 @@ DThinker *FLevelLocals::FirstThinker (int statnum)
return Thinkers.FirstThinker(statnum);
}
DThinker* FLevelLocals::FirstClientsideThinker(int statnum)
{
return ClientsideThinkers.FirstThinker(statnum);
}
//==========================================================================
//
//
@ -797,7 +862,10 @@ void DThinker::ChangeStatNum (int statnum)
statnum = MAX_STATNUM;
}
Remove();
Level->Thinkers.Link(this, statnum);
if (IsClientside())
Level->ClientsideThinkers.Link(this, statnum);
else
Level->Thinkers.Link(this, statnum);
}
static void ChangeStatNum(DThinker *thinker, int statnum)
@ -923,8 +991,9 @@ size_t DThinker::PropagateMark()
//
//==========================================================================
FThinkerIterator::FThinkerIterator (FLevelLocals *l, const PClass *type, int statnum) : Level(l)
FThinkerIterator::FThinkerIterator (FLevelLocals *l, const PClass *type, int statnum, bool clientside) : Level(l)
{
m_ThinkerPool = clientside ? &Level->ClientsideThinkers : &Level->Thinkers;
if ((unsigned)statnum > MAX_STATNUM)
{
m_Stat = STAT_FIRST_THINKING;
@ -945,8 +1014,9 @@ FThinkerIterator::FThinkerIterator (FLevelLocals *l, const PClass *type, int sta
//
//==========================================================================
FThinkerIterator::FThinkerIterator (FLevelLocals *l, const PClass *type, int statnum, DThinker *prev) : Level(l)
FThinkerIterator::FThinkerIterator (FLevelLocals *l, const PClass *type, int statnum, DThinker *prev, bool clientside) : Level(l)
{
m_ThinkerPool = clientside ? &Level->ClientsideThinkers : &Level->Thinkers;
if ((unsigned)statnum > MAX_STATNUM)
{
m_Stat = STAT_FIRST_THINKING;
@ -977,7 +1047,7 @@ FThinkerIterator::FThinkerIterator (FLevelLocals *l, const PClass *type, int sta
void FThinkerIterator::Reinit ()
{
m_CurrThinker = Level->Thinkers.Thinkers[m_Stat].GetHead();
m_CurrThinker = m_ThinkerPool->Thinkers[m_Stat].GetHead();
m_SearchingFresh = false;
}
@ -1018,7 +1088,7 @@ DThinker *FThinkerIterator::Next (bool exact)
}
if ((m_SearchingFresh = !m_SearchingFresh))
{
m_CurrThinker = Level->Thinkers.FreshThinkers[m_Stat].GetHead();
m_CurrThinker = m_ThinkerPool->FreshThinkers[m_Stat].GetHead();
}
} while (m_SearchingFresh);
if (m_SearchStats)
@ -1029,7 +1099,7 @@ DThinker *FThinkerIterator::Next (bool exact)
m_Stat = STAT_FIRST_THINKING;
}
}
m_CurrThinker = Level->Thinkers.Thinkers[m_Stat].GetHead();
m_CurrThinker = m_ThinkerPool->Thinkers[m_Stat].GetHead();
m_SearchingFresh = false;
} while (m_SearchStats && m_Stat != STAT_FIRST_THINKING);
return nullptr;

View file

@ -79,6 +79,7 @@ struct FThinkerCollection
}
void RunThinkers(FLevelLocals *Level); // The level is needed to tick the lights
void RunClientsideThinkers(FLevelLocals* Level);
void DestroyAllThinkers(bool fullgc = true);
void SerializeThinkers(FSerializer &arc, bool keepPlayers);
void MarkRoots();
@ -132,14 +133,15 @@ protected:
const PClass *m_ParentType;
private:
FLevelLocals *Level;
FThinkerCollection* m_ThinkerPool;
DThinker *m_CurrThinker;
uint8_t m_Stat;
bool m_SearchStats;
bool m_SearchingFresh;
public:
FThinkerIterator (FLevelLocals *Level, const PClass *type, int statnum=MAX_STATNUM+1);
FThinkerIterator (FLevelLocals *Level, const PClass *type, int statnum, DThinker *prev);
FThinkerIterator (FLevelLocals *Level, const PClass *type, int statnum=MAX_STATNUM+1, bool clientside = false);
FThinkerIterator (FLevelLocals *Level, const PClass *type, int statnum, DThinker *prev, bool clientside = false);
DThinker *Next (bool exact = false);
void Reinit ();
};
@ -147,19 +149,19 @@ public:
template <class T> class TThinkerIterator : public FThinkerIterator
{
public:
TThinkerIterator (FLevelLocals *Level, int statnum=MAX_STATNUM+1) : FThinkerIterator (Level, RUNTIME_CLASS(T), statnum)
TThinkerIterator (FLevelLocals *Level, int statnum=MAX_STATNUM+1, bool clientside = false) : FThinkerIterator (Level, RUNTIME_CLASS(T), statnum, clientside)
{
}
TThinkerIterator (FLevelLocals *Level, int statnum, DThinker *prev) : FThinkerIterator (Level, RUNTIME_CLASS(T), statnum, prev)
TThinkerIterator (FLevelLocals *Level, int statnum, DThinker *prev, bool clientside = false) : FThinkerIterator (Level, RUNTIME_CLASS(T), statnum, prev, clientside)
{
}
TThinkerIterator (FLevelLocals *Level, const PClass *subclass, int statnum=MAX_STATNUM+1) : FThinkerIterator(Level, subclass, statnum)
TThinkerIterator (FLevelLocals *Level, const PClass *subclass, int statnum=MAX_STATNUM+1, bool clientside = false) : FThinkerIterator(Level, subclass, statnum, clientside)
{
}
TThinkerIterator (FLevelLocals *Level, FName subclass, int statnum=MAX_STATNUM+1) : FThinkerIterator(Level, PClass::FindClass(subclass), statnum)
TThinkerIterator (FLevelLocals *Level, FName subclass, int statnum=MAX_STATNUM+1, bool clientside = false) : FThinkerIterator(Level, PClass::FindClass(subclass), statnum, clientside)
{
}
TThinkerIterator (FLevelLocals *Level, FName subclass, int statnum, DThinker *prev) : FThinkerIterator(Level, PClass::FindClass(subclass), statnum, prev)
TThinkerIterator (FLevelLocals *Level, FName subclass, int statnum, DThinker *prev, bool clientside = false) : FThinkerIterator(Level, PClass::FindClass(subclass), statnum, prev, clientside)
{
}
T *Next (bool exact = false)

View file

@ -653,6 +653,11 @@ struct CallReturn
};
static bool IsClientSideScript(const ScriptPtr& script)
{
return (script.Flags & SCRIPTF_ClientSide);
}
class DLevelScript : public DObject
{
DECLARE_CLASS(DLevelScript, DObject)
@ -675,7 +680,7 @@ public:
};
DLevelScript(FLevelLocals *l, AActor *who, line_t *where, int num, const ScriptPtr *code, FBehavior *module,
const int *args, int argcount, int flags);
const int *args, int argcount, int flags, bool clientside);
void Serialize(FSerializer &arc);
int RunScript();
@ -700,6 +705,7 @@ public:
protected:
DLevelScript *next, *prev;
TObjPtr<DACSThinker*> controller;
int script;
TArray<int32_t> Localvars;
int *pc;
@ -783,7 +789,7 @@ private:
};
static DLevelScript *P_GetScriptGoing (FLevelLocals *Level, AActor *who, line_t *where, int num, const ScriptPtr *code, FBehavior *module,
const int *args, int argcount, int flags);
const int *args, int argcount, int flags, bool clientside);
struct FBehavior::ArrayInfo
@ -2015,6 +2021,13 @@ void FBehaviorContainer::MarkLevelVarStrings()
script->MarkLocalVarStrings();
}
}
if (Level->ClientSideACSThinker != nullptr)
{
for (DLevelScript* script = Level->ClientSideACSThinker->Scripts; script != NULL; script = script->GetNext())
{
script->MarkLocalVarStrings();
}
}
}
void FBehaviorContainer::LockLevelVarStrings(int levelnum)
@ -2032,6 +2045,13 @@ void FBehaviorContainer::LockLevelVarStrings(int levelnum)
script->LockLocalVarStrings(levelnum);
}
}
if (Level->ClientSideACSThinker != nullptr)
{
for (DLevelScript* script = Level->ClientSideACSThinker->Scripts; script != NULL; script = script->GetNext())
{
script->LockLocalVarStrings(levelnum);
}
}
}
void FBehaviorContainer::UnlockLevelVarStrings(int levelnum)
@ -3307,7 +3327,7 @@ void FBehavior::StartTypedScripts (uint16_t type, AActor *activator, bool always
if (ptr->Type == type)
{
DLevelScript *runningScript = P_GetScriptGoing (Level, activator, NULL, ptr->Number,
ptr, this, &arg1, 1, always ? ACS_ALWAYS : 0);
ptr, this, &arg1, 1, always ? ACS_ALWAYS : 0, IsClientSideScript(*ptr));
if (nullptr != runningScript && runNow)
{
runningScript->RunScript();
@ -3330,6 +3350,12 @@ void FBehaviorContainer::StopMyScripts (AActor *actor)
{
controller->StopScriptsFor (actor);
}
controller = actor->Level->ClientSideACSThinker;
if (controller != NULL)
{
controller->StopScriptsFor(actor);
}
}
@ -3510,8 +3536,6 @@ void DLevelScript::Serialize(FSerializer &arc)
void DLevelScript::Unlink ()
{
DACSThinker *controller = Level->ACSThinker;
if (controller->LastScript == this)
{
controller->LastScript = prev;
@ -3536,8 +3560,6 @@ void DLevelScript::Unlink ()
void DLevelScript::Link ()
{
DACSThinker *controller = Level->ACSThinker;
next = controller->Scripts;
GC::WriteBarrier(this, next);
if (controller->Scripts)
@ -3556,8 +3578,6 @@ void DLevelScript::Link ()
void DLevelScript::PutLast ()
{
DACSThinker *controller = Level->ACSThinker;
if (controller->LastScript == this)
return;
@ -3578,8 +3598,6 @@ void DLevelScript::PutLast ()
void DLevelScript::PutFirst ()
{
DACSThinker *controller = Level->ACSThinker;
if (controller->Scripts == this)
return;
@ -5856,11 +5874,11 @@ int DLevelScript::CallFunction(int argCount, int funcIndex, int32_t *args, int &
break;
case ACSF_UniqueTID:
return Level->FindUniqueTID(argCount > 0 ? args[0] : 0, (argCount > 1 && args[1] >= 0) ? args[1] : 0);
return Level->FindUniqueTID(argCount > 0 ? args[0] : 0, (argCount > 1 && args[1] >= 0) ? args[1] : 0, false);
case ACSF_IsTIDUsed:
MIN_ARG_COUNT(1);
return Level->IsTIDUsed(args[0]);
return Level->IsTIDUsed(args[0], false);
case ACSF_Sqrt:
MIN_ARG_COUNT(1);
@ -6902,7 +6920,6 @@ PClass *DLevelScript::GetClassForIndex(int index) const
int DLevelScript::RunScript()
{
DACSThinker *controller = Level->ACSThinker;
ACSLocalVariables locals(Localvars);
ACSLocalArrays noarrays;
ACSLocalArrays *localarrays = &noarrays;
@ -10389,9 +10406,9 @@ scriptwait:
#undef PushtoStack
static DLevelScript *P_GetScriptGoing (FLevelLocals *l, AActor *who, line_t *where, int num, const ScriptPtr *code, FBehavior *module,
const int *args, int argcount, int flags)
const int *args, int argcount, int flags, bool clientside)
{
DACSThinker *controller = l->ACSThinker;
DACSThinker *controller = clientside ? l->ClientSideACSThinker : l->ACSThinker;
DLevelScript **running;
if (controller && !(flags & ACS_ALWAYS) && (running = controller->RunningScripts.CheckKey(num)) != NULL)
@ -10404,16 +10421,28 @@ static DLevelScript *P_GetScriptGoing (FLevelLocals *l, AActor *who, line_t *whe
return NULL;
}
return Create<DLevelScript> (l, who, where, num, code, module, args, argcount, flags);
return Create<DLevelScript> (l, who, where, num, code, module, args, argcount, flags, clientside);
}
DLevelScript::DLevelScript (FLevelLocals *l, AActor *who, line_t *where, int num, const ScriptPtr *code, FBehavior *module,
const int *args, int argcount, int flags)
const int *args, int argcount, int flags, bool clientside)
: activeBehavior (module)
{
Level = l;
if (Level->ACSThinker == nullptr)
Level->ACSThinker = Level->CreateThinker<DACSThinker>();
if (clientside)
{
if (Level->ClientSideACSThinker == nullptr)
Level->ClientSideACSThinker = Level->CreateClientsideThinker<DACSThinker>();
controller = Level->ClientSideACSThinker;
}
else
{
if (Level->ACSThinker == nullptr)
Level->ACSThinker = Level->CreateThinker<DACSThinker>();
controller = Level->ACSThinker;
}
script = num;
assert(code->VarCount >= code->ArgCount);
@ -10440,11 +10469,11 @@ DLevelScript::DLevelScript (FLevelLocals *l, AActor *who, line_t *where, int num
// goes by while they're in their default state.
if (!(flags & ACS_ALWAYS))
Level->ACSThinker->RunningScripts[num] = this;
controller->RunningScripts[num] = this;
Link();
if (Level->flags2 & LEVEL2_HEXENHACK)
if (!clientside && (Level->flags2 & LEVEL2_HEXENHACK))
{
PutLast();
}
@ -10452,14 +10481,20 @@ DLevelScript::DLevelScript (FLevelLocals *l, AActor *who, line_t *where, int num
DPrintf(DMSG_SPAMMY, "%s started.\n", ScriptPresentation(num).GetChars());
}
void SetScriptState (DACSThinker *controller, int script, DLevelScript::EScriptState state)
void SetScriptState (FLevelLocals& level, int script, DLevelScript::EScriptState state)
{
DLevelScript **running;
auto controller = level.ACSThinker;
if (controller != NULL && (running = controller->RunningScripts.CheckKey(script)) != NULL)
{
(*running)->SetState (state);
return;
}
controller = level.ClientSideACSThinker;
if (controller != NULL && (running = controller->RunningScripts.CheckKey(script)) != NULL)
(*running)->SetState(state);
}
void FLevelLocals::DoDeferedScripts ()
@ -10471,33 +10506,32 @@ void FLevelLocals::DoDeferedScripts ()
for(int i = info->deferred.Size()-1; i>=0; i--)
{
acsdefered_t *def = &info->deferred[i];
scriptdata = Behaviors.FindScript(def->script, module);
if (scriptdata == nullptr)
{
Printf("P_DoDeferredScripts: Unknown %s\n", ScriptPresentation(def->script).GetChars());
continue;
}
switch (def->type)
{
case acsdefered_t::defexecute:
case acsdefered_t::defexealways:
scriptdata = Behaviors.FindScript (def->script, module);
if (scriptdata)
{
P_GetScriptGoing (this, (unsigned)def->playernum < MAXPLAYERS &&
PlayerInGame(def->playernum) ? Players[def->playernum]->mo : nullptr,
nullptr, def->script,
scriptdata, module,
def->args, 3,
def->type == acsdefered_t::defexealways ? ACS_ALWAYS : 0);
}
else
{
Printf ("P_DoDeferredScripts: Unknown %s\n", ScriptPresentation(def->script).GetChars());
}
P_GetScriptGoing (this, (unsigned)def->playernum < MAXPLAYERS &&
PlayerInGame(def->playernum) ? Players[def->playernum]->mo : nullptr,
nullptr, def->script,
scriptdata, module,
def->args, 3,
def->type == acsdefered_t::defexealways ? ACS_ALWAYS : 0, IsClientSideScript(*scriptdata));
break;
case acsdefered_t::defsuspend:
SetScriptState (ACSThinker, def->script, DLevelScript::SCRIPT_Suspended);
SetScriptState (*this, def->script, DLevelScript::SCRIPT_Suspended);
DPrintf (DMSG_SPAMMY, "Deferred suspend of %s\n", ScriptPresentation(def->script).GetChars());
break;
case acsdefered_t::defterminate:
SetScriptState (ACSThinker, def->script, DLevelScript::SCRIPT_PleaseRemove);
SetScriptState (*this, def->script, DLevelScript::SCRIPT_PleaseRemove);
DPrintf (DMSG_SPAMMY, "Deferred terminate of %s\n", ScriptPresentation(def->script).GetChars());
break;
}
@ -10561,8 +10595,15 @@ int P_StartScript (FLevelLocals *Level, AActor *who, line_t *where, int script,
return false;
}
}
DLevelScript *runningScript = P_GetScriptGoing (Level, who, where, script,
scriptdata, module, args, argcount, flags);
DLevelScript* runningScript = nullptr;
const bool clientside = IsClientSideScript(*scriptdata);
if (!(flags & ACS_NET) || !clientside || (who && Level->isConsolePlayer(who->player->mo)))
{
runningScript = P_GetScriptGoing(Level, who, where, script,
scriptdata, module, args, argcount, flags, clientside);
}
if (runningScript != NULL)
{
if (flags & ACS_WANTRESULT)
@ -10596,7 +10637,7 @@ void P_SuspendScript (FLevelLocals *Level, int script, const char *map)
if (strnicmp (Level->MapName.GetChars(), map, 8))
addDefered (FindLevelInfo (map), acsdefered_t::defsuspend, script, NULL, 0, NULL);
else
SetScriptState (Level->ACSThinker, script, DLevelScript::SCRIPT_Suspended);
SetScriptState (*Level, script, DLevelScript::SCRIPT_Suspended);
}
void P_TerminateScript (FLevelLocals *Level, int script, const char *map)
@ -10604,7 +10645,7 @@ void P_TerminateScript (FLevelLocals *Level, int script, const char *map)
if (strnicmp (Level->MapName.GetChars(), map, 8))
addDefered (FindLevelInfo (map), acsdefered_t::defterminate, script, NULL, 0, NULL);
else
SetScriptState (Level->ACSThinker, script, DLevelScript::SCRIPT_PleaseRemove);
SetScriptState (*Level, script, DLevelScript::SCRIPT_PleaseRemove);
}
FSerializer &Serialize(FSerializer &arc, const char *key, acsdefered_t &defer, acsdefered_t *def)

View file

@ -339,7 +339,8 @@ enum
// Script flags
enum
{
SCRIPTF_Net = 0x0001 // Safe to "puke" in multiplayer
SCRIPTF_Net = 0x0001, // Safe to "puke" in multiplayer
SCRIPTF_ClientSide = 0x0002, // Executed locally for clients but not across them
};
enum ACSFormat { ACS_Old, ACS_Enhanced, ACS_LittleEnhanced, ACS_Unknown };

View file

@ -294,38 +294,36 @@ void AActor::UnlinkFromWorld (FLinkContext *ctx)
// killough 8/11/98: simpler scheme using pointers-to-pointers for prev
// pointers, allows head node pointers to be treated like everything else
AActor **prev = sprev;
AActor *next = snext;
if (prev != NULL) // prev will be NULL if this actor gets deleted due to cleaning up from a broken savegame
if (prev != NULL)
{
AActor* next = snext;
if ((*prev = next)) // unlink from sector list
next->sprev = prev;
snext = NULL;
sprev = (AActor **)(size_t)0xBeefCafe; // Woo! Bug-catching value!
// phares 3/14/98
//
// Save the sector list pointed to by touching_sectorlist.
// In P_SetThingPosition, we'll keep any nodes that represent
// sectors the Thing still touches. We'll add new ones then, and
// delete any nodes for sectors the Thing has vacated. Then we'll
// put it back into touching_sectorlist. It's done this way to
// avoid a lot of deleting/creating for nodes, when most of the
// time you just get back what you deleted anyway.
if (ctx != nullptr)
{
ctx->sector_list = touching_sectorlist;
ctx->render_list = touching_rendersectors;
}
else
{
P_DelSeclist(touching_sectorlist, &sector_t::touching_thinglist);
P_DelSeclist(touching_rendersectors, &sector_t::touching_renderthings);
}
touching_sectorlist = nullptr; //to be restored by P_SetThingPosition
touching_rendersectors = nullptr;
}
// phares 3/14/98
//
// Save the sector list pointed to by touching_sectorlist.
// In P_SetThingPosition, we'll keep any nodes that represent
// sectors the Thing still touches. We'll add new ones then, and
// delete any nodes for sectors the Thing has vacated. Then we'll
// put it back into touching_sectorlist. It's done this way to
// avoid a lot of deleting/creating for nodes, when most of the
// time you just get back what you deleted anyway.
if (ctx != nullptr)
{
ctx->sector_list = touching_sectorlist;
ctx->render_list = touching_rendersectors;
}
else
{
P_DelSeclist(touching_sectorlist, &sector_t::touching_thinglist);
P_DelSeclist(touching_rendersectors, &sector_t::touching_renderthings);
}
touching_sectorlist = nullptr; //to be restored by P_SetThingPosition
touching_rendersectors = nullptr;
}
if (!(flags & MF_NOBLOCKMAP))
@ -469,32 +467,37 @@ void AActor::LinkToWorld(FLinkContext *ctx, bool spawningmapthing, sector_t *sec
subsector = Level->PointInRenderSubsector(Pos()); // this is from the rendering nodes, not the gameplay nodes!
section = subsector->section;
const bool clientside = IsClientside();
if (!(flags & MF_NOSECTOR))
{
// invisible things don't go into the sector links
// killough 8/11/98: simpler scheme using pointer-to-pointer prev
// pointers, allows head nodes to be treated like everything else
if (!clientside)
{
// invisible things don't go into the sector links
// killough 8/11/98: simpler scheme using pointer-to-pointer prev
// pointers, allows head nodes to be treated like everything else
AActor **link = &sector->thinglist;
AActor *next = *link;
if ((snext = next))
next->sprev = &snext;
sprev = link;
*link = this;
AActor** link = &sector->thinglist;
AActor* next = *link;
if ((snext = next))
next->sprev = &snext;
sprev = link;
*link = this;
// phares 3/16/98
//
// If sector_list isn't NULL, it has a collection of sector
// nodes that were just removed from this Thing.
// phares 3/16/98
//
// If sector_list isn't NULL, it has a collection of sector
// nodes that were just removed from this Thing.
// Collect the sectors the object will live in by looking at
// the existing sector_list and adding new nodes and deleting
// obsolete ones.
// Collect the sectors the object will live in by looking at
// the existing sector_list and adding new nodes and deleting
// obsolete ones.
// When a node is deleted, its sector links (the links starting
// at sector_t->touching_thinglist) are broken. When a node is
// added, new sector links are created.
touching_sectorlist = P_CreateSecNodeList(this, radius, ctx != nullptr ? ctx->sector_list : nullptr, &sector_t::touching_thinglist); // Attach to thing
}
// When a node is deleted, its sector links (the links starting
// at sector_t->touching_thinglist) are broken. When a node is
// added, new sector links are created.
touching_sectorlist = P_CreateSecNodeList(this, radius, ctx != nullptr? ctx->sector_list : nullptr, &sector_t::touching_thinglist); // Attach to thing
if (renderradius >= 0) touching_rendersectors = P_CreateSecNodeList(this, RenderRadius(), ctx != nullptr ? ctx->render_list : nullptr, &sector_t::touching_renderthings);
else
{
@ -505,7 +508,7 @@ void AActor::LinkToWorld(FLinkContext *ctx, bool spawningmapthing, sector_t *sec
// link into blockmap (inert things don't need to be in the blockmap)
if (!(flags & MF_NOBLOCKMAP))
if (!(flags & MF_NOBLOCKMAP) && !clientside)
{
FPortalGroupArray check;

View file

@ -143,6 +143,7 @@ static FRandom pr_uniquetid("UniqueTID");
// PUBLIC DATA DEFINITIONS -------------------------------------------------
FRandom pr_spawnmobj ("SpawnActor");
FCRandom pr_spawncsmobj("SpawnClientsideActor");
FRandom pr_bounce("Bounce");
FRandom pr_spawnmissile("SpawnMissile");
@ -196,9 +197,9 @@ DEFINE_FIELD(DBehavior, Level)
void AActor::EnableNetworking(const bool enable)
{
if (!enable)
if (!enable && !IsClientside())
{
ThrowAbortException(X_OTHER, "Cannot disable networking on Actors. Consider a Thinker instead.");
ThrowAbortException(X_OTHER, "Cannot disable networking on Actors. Consider a Thinker or clientside Actor instead.");
return;
}
@ -844,7 +845,7 @@ bool AActor::IsMapActor()
inline int GetTics(AActor* actor, FState * newstate)
{
int tics = newstate->GetTics();
int tics = actor->IsClientside() ? newstate->GetClientsideTics() : newstate->GetTics();
if (actor->isFast() && newstate->GetFast())
{
return tics - (tics>>1);
@ -3484,7 +3485,7 @@ void AActor::AddToHash ()
else
{
int hash = TIDHASH (tid);
auto &slot = Level->TIDHash[hash];
auto &slot = IsClientside() ? Level->ClientSideTIDHash[hash] : Level->TIDHash[hash];
inext = slot;
iprev = &slot;
@ -3542,9 +3543,9 @@ void AActor::SetTID (int newTID)
//
//==========================================================================
bool FLevelLocals::IsTIDUsed(int tid)
bool FLevelLocals::IsTIDUsed(int tid, bool clientside)
{
AActor *probe = TIDHash[tid & 127];
AActor *probe = clientside ? ClientSideTIDHash[tid & 127] : TIDHash[tid & 127];
while (probe != NULL)
{
if (probe->tid == tid)
@ -3567,7 +3568,7 @@ bool FLevelLocals::IsTIDUsed(int tid)
//
//==========================================================================
int FLevelLocals::FindUniqueTID(int start_tid, int limit)
int FLevelLocals::FindUniqueTID(int start_tid, int limit, bool clientside)
{
int tid;
@ -3583,7 +3584,7 @@ int FLevelLocals::FindUniqueTID(int start_tid, int limit)
}
for (tid = start_tid; tid <= limit; ++tid)
{
if (tid != 0 && !IsTIDUsed(tid))
if (tid != 0 && !IsTIDUsed(tid, clientside))
{
return tid;
}
@ -3605,7 +3606,7 @@ int FLevelLocals::FindUniqueTID(int start_tid, int limit)
{
// Use a positive starting TID.
tid = pr_uniquetid.GenRand32() & INT_MAX;
tid = FindUniqueTID(tid == 0 ? 1 : tid, 5);
tid = FindUniqueTID(tid == 0 ? 1 : tid, 5, clientside);
if (tid != 0)
{
return tid;
@ -3621,7 +3622,7 @@ CCMD(utid)
for (auto Level : AllLevels())
{
Printf("%s, %d\n", Level->MapName.GetChars(), Level->FindUniqueTID(argv.argc() > 1 ? atoi(argv[1]) : 0,
(argv.argc() > 2 && atoi(argv[2]) >= 0) ? atoi(argv[2]) : 0));
(argv.argc() > 2 && atoi(argv[2]) >= 0) ? atoi(argv[2]) : 0, false));
}
}
@ -5113,6 +5114,7 @@ DEFINE_ACTION_FUNCTION(AActor, UpdateWaterLevel)
void ConstructActor(AActor *actor, const DVector3 &pos, bool SpawningMapThing)
{
const bool clientside = actor->IsClientside();
auto Level = actor->Level;
actor->SpawnTime = Level->totaltime;
actor->SpawnOrder = Level->spawnindex++;
@ -5136,7 +5138,7 @@ void ConstructActor(AActor *actor, const DVector3 &pos, bool SpawningMapThing)
// Actors with zero gravity need the NOGRAVITY flag set.
if (actor->Gravity == 0) actor->flags |= MF_NOGRAVITY;
FRandom &rng = Level->BotInfo.m_Thinking ? pr_botspawnmobj : pr_spawnmobj;
FRandom &rng = clientside ? pr_spawncsmobj : (Level->BotInfo.m_Thinking ? pr_botspawnmobj : pr_spawnmobj);
if ((!!G_SkillProperty(SKILLP_InstantReaction) || actor->flags5 & MF5_ALWAYSFAST || !!(dmflags & DF_INSTANT_REACTION))
&& actor->flags3 & MF3_ISMONSTER)
@ -5153,7 +5155,7 @@ void ConstructActor(AActor *actor, const DVector3 &pos, bool SpawningMapThing)
// routine, it will not be called.
FState *st = actor->SpawnState;
actor->state = st;
actor->tics = st->GetTics();
actor->tics = clientside ? st->GetClientsideTics() : st->GetTics();
actor->sprite = st->sprite;
actor->frame = st->GetFrame();
@ -5308,8 +5310,15 @@ AActor *AActor::StaticSpawn(FLevelLocals *Level, PClassActor *type, const DVecto
AActor *actor;
actor = static_cast<AActor *>(Level->CreateThinker(type));
actor->EnableNetworking(true);
if (GetDefaultByType(type)->ObjectFlags & OF_ClientSide)
{
actor = static_cast<AActor*>(Level->CreateClientsideThinker(type));
}
else
{
actor = static_cast<AActor*>(Level->CreateThinker(type));
actor->EnableNetworking(true);
}
ConstructActor(actor, pos, SpawningMapThing);
return actor;
@ -5326,6 +5335,28 @@ DEFINE_ACTION_FUNCTION(AActor, Spawn)
ACTION_RETURN_OBJECT(AActor::StaticSpawn(currentVMLevel, type, DVector3(x, y, z), replace_t(flags)));
}
static AActor* SpawnClientside(PClassActor* type, double x, double y, double z, int flags)
{
if (!(GetDefaultByType(type)->ObjectFlags & OF_ClientSide))
{
ThrowAbortException(X_OTHER, "Tried to spawn a non-clientside Actor from a clientside spawn function.");
return nullptr;
}
return AActor::StaticSpawn(currentVMLevel, type, { x, y, z }, (replace_t)flags);
}
DEFINE_ACTION_FUNCTION_NATIVE(AActor, SpawnClientside, SpawnClientside)
{
PARAM_PROLOGUE;
PARAM_CLASS_NOT_NULL(type, AActor);
PARAM_FLOAT(x);
PARAM_FLOAT(y);
PARAM_FLOAT(z);
PARAM_INT(flags);
ACTION_RETURN_OBJECT(SpawnClientside(type, x, y, z, flags));
}
PClassActor *ClassForSpawn(FName classname)
{
PClass *cls = PClass::FindClass(classname);

View file

@ -157,15 +157,6 @@ static TArray<AActor *> PredictionSectorListBackup;
static TArray<sector_t *> PredictionTouchingSectorsBackup;
static TArray<msecnode_t *> PredictionTouchingSectors_sprev_Backup;
static TArray<sector_t *> PredictionRenderSectorsBackup;
static TArray<msecnode_t *> PredictionRenderSectors_sprev_Backup;
static TArray<sector_t *> PredictionPortalSectorsBackup;
static TArray<msecnode_t *> PredictionPortalSectors_sprev_Backup;
static TArray<FLinePortal *> PredictionPortalLinesBackup;
static TArray<portnode_t *> PredictionPortalLines_sprev_Backup;
struct
{
DVector3 Pos = {};
@ -1381,19 +1372,9 @@ void BackupNodeList(AActor *act, nodetype *head, nodetype *linktype::*otherlist,
}
template<class nodetype, class linktype>
nodetype *RestoreNodeList(AActor *act, nodetype *head, nodetype *linktype::*otherlist, TArray<nodetype*, nodetype*> &prevbackup, TArray<linktype *, linktype *> &otherbackup)
nodetype *RestoreNodeList(AActor *act, nodetype *linktype::*otherlist, TArray<nodetype*, nodetype*> &prevbackup, TArray<linktype *, linktype *> &otherbackup)
{
// Destroy old refrences
nodetype *node = head;
while (node)
{
node->m_thing = NULL;
node = node->m_tnext;
}
// Make the sector_list match the player's touching_sectorlist before it got predicted.
P_DelSeclist(head, otherlist);
head = NULL;
nodetype* head = NULL;
for (auto i = otherbackup.Size(); i-- > 0;)
{
head = P_AddSecnode(otherbackup[i], act, head, otherbackup[i]->*otherlist);
@ -1402,7 +1383,7 @@ nodetype *RestoreNodeList(AActor *act, nodetype *head, nodetype *linktype::*othe
//ctx.sector_list = NULL; // clear for next time
// In the old code this block never executed because of the commented-out NULL assignment above. Needs to be checked
node = head;
nodetype* node = head;
while (node)
{
if (node->m_thing == NULL)
@ -1496,9 +1477,6 @@ void P_PredictPlayer (player_t *player)
player->cheats |= CF_PREDICTING;
BackupNodeList(act, act->touching_sectorlist, &sector_t::touching_thinglist, PredictionTouchingSectors_sprev_Backup, PredictionTouchingSectorsBackup);
BackupNodeList(act, act->touching_rendersectors, &sector_t::touching_renderthings, PredictionRenderSectors_sprev_Backup, PredictionRenderSectorsBackup);
BackupNodeList(act, act->touching_sectorportallist, &sector_t::sectorportal_thinglist, PredictionPortalSectors_sprev_Backup, PredictionPortalSectorsBackup);
BackupNodeList(act, act->touching_lineportallist, &FLinePortal::lineportal_thinglist, PredictionPortalLines_sprev_Backup, PredictionPortalLinesBackup);
// Keep an ordered list off all actors in the linked sector.
PredictionSectorListBackup.Clear();
@ -1637,15 +1615,15 @@ void P_UnPredictPlayer ()
// could cause it to change during prediction.
player->camera = savedcamera;
FLinkContext ctx;
// Unlink from all list, including those which are not being handled by UnlinkFromWorld.
auto sectorportal_list = act->touching_sectorportallist;
auto lineportal_list = act->touching_lineportallist;
act->touching_sectorportallist = nullptr;
act->touching_lineportallist = nullptr;
act->UnlinkFromWorld(&ctx);
// Unlink from all lists
act->UnlinkFromWorld(nullptr);
memcpy(&act->snext, PredictionActorBackupArray.Data(), PredictionActorBackupArray.Size() - ((uint8_t *)&act->snext - (uint8_t *)act));
// Clear stale pointers. The blockmap node is kept since it's the one that will be relinked back into the blockmap. Given
// it was removed from the list without being freed before predicting it's still valid.
act->touching_lineportallist = nullptr;
act->touching_rendersectors = act->touching_sectorlist = act->touching_sectorportallist = nullptr;
act->sprev = (AActor**)(size_t)0xBeefCafe;
act->snext = nullptr;
if (act->ViewPos != nullptr)
{
@ -1678,15 +1656,15 @@ void P_UnPredictPlayer ()
*link = me;
}
act->touching_sectorlist = RestoreNodeList(act, ctx.sector_list, &sector_t::touching_thinglist, PredictionTouchingSectors_sprev_Backup, PredictionTouchingSectorsBackup);
act->touching_rendersectors = RestoreNodeList(act, ctx.render_list, &sector_t::touching_renderthings, PredictionRenderSectors_sprev_Backup, PredictionRenderSectorsBackup);
act->touching_sectorportallist = RestoreNodeList(act, sectorportal_list, &sector_t::sectorportal_thinglist, PredictionPortalSectors_sprev_Backup, PredictionPortalSectorsBackup);
act->touching_lineportallist = RestoreNodeList(act, lineportal_list, &FLinePortal::lineportal_thinglist, PredictionPortalLines_sprev_Backup, PredictionPortalLinesBackup);
// Only the touching list actually needs to be restored to avoid impacting gameplay. The rest is just clientside fluff that can
// be handled by relinking.
act->touching_sectorlist = RestoreNodeList(act, &sector_t::touching_thinglist, PredictionTouchingSectors_sprev_Backup, PredictionTouchingSectorsBackup);
if (act->renderradius >= 0.0)
act->touching_rendersectors = P_CreateSecNodeList(act, act->RenderRadius(), nullptr, &sector_t::touching_renderthings);
}
// Now fix the pointers in the blocknode chain
FBlockNode *block = act->BlockNode;
while (block != NULL)
{
*(block->PrevActor) = block;
@ -1697,6 +1675,8 @@ void P_UnPredictPlayer ()
block = block->NextBlock;
}
act->UpdateRenderSectorList();
actInvSel = InvSel;
player->inventorytics = inventorytics;
}

View file

@ -75,6 +75,7 @@ extern float BackbuttonAlpha;
#define DEFINE_FLAG(prefix, name, type, variable) { (unsigned int)prefix##_##name, #name, (int)(size_t)&((type*)1)->variable - 1, sizeof(((type *)0)->variable), VARF_Native }
#define DEFINE_PROTECTED_FLAG(prefix, name, type, variable) { (unsigned int)prefix##_##name, #name, (int)(size_t)&((type*)1)->variable - 1, sizeof(((type *)0)->variable), VARF_Native|VARF_ReadOnly|VARF_InternalAccess }
#define DEFINE_FLAG2(symbol, name, type, variable) { (unsigned int)symbol, #name, (int)(size_t)&((type*)1)->variable - 1, sizeof(((type *)0)->variable), VARF_Native }
#define DEFINE_PROTECTED_FLAG2(symbol, name, type, variable) { (unsigned int)symbol, #name, (int)(size_t)&((type*)1)->variable - 1, sizeof(((type *)0)->variable), VARF_Native|VARF_ReadOnly|VARF_InternalAccess }
#define DEFINE_FLAG2_DEPRECATED(symbol, name, type, variable, version) { (unsigned int)symbol, #name, (int)(size_t)&((type*)1)->variable - 1, sizeof(((type *)0)->variable), VARF_Native|VARF_Deprecated }
#define DEFINE_DEPRECATED_FLAG(name, version) { DEPF_##name, #name, -1, 0, VARF_Deprecated, version }
#define DEFINE_DUMMY_FLAG(name, deprec) { DEPF_UNUSED, #name, -1, 0, deprec? VARF_Deprecated:0 }
@ -412,6 +413,7 @@ static FFlagDef ActorFlagDefs[]=
DEFINE_FLAG2(BOUNCE_ModifyPitch, BOUNCEMODIFIESPITCH, AActor, BounceFlags),
DEFINE_FLAG2(OF_Transient, NOSAVEGAME, AActor, ObjectFlags),
DEFINE_PROTECTED_FLAG2(OF_ClientSide, CLIENTSIDE, AActor, ObjectFlags),
// Deprecated flags which need a ZScript workaround.
DEFINE_DEPRECATED_FLAG(MISSILEMORE, MakeVersion(4, 13, 0)),
@ -463,7 +465,6 @@ static FFlagDef MoreFlagDefs[] =
// [BB] New DECORATE network related flag defines here.
DEFINE_DUMMY_FLAG(NONETID, false),
DEFINE_DUMMY_FLAG(ALLOWCLIENTSPAWN, false),
DEFINE_DUMMY_FLAG(CLIENTSIDEONLY, false),
DEFINE_DUMMY_FLAG(SERVERSIDEONLY, false),
};

View file

@ -41,8 +41,8 @@ class DThinkerIterator : public DObject, public FThinkerIterator
DECLARE_ABSTRACT_CLASS(DThinkerIterator, DObject)
public:
DThinkerIterator(FLevelLocals *Level, PClass *cls, int statnum = MAX_STATNUM + 1)
: FThinkerIterator(Level, cls, statnum)
DThinkerIterator(FLevelLocals *Level, PClass *cls, int statnum = MAX_STATNUM + 1, bool clientside = false)
: FThinkerIterator(Level, cls, statnum, clientside)
{
}
};
@ -62,6 +62,19 @@ DEFINE_ACTION_FUNCTION_NATIVE(DThinkerIterator, Create, CreateThinkerIterator)
ACTION_RETURN_OBJECT(CreateThinkerIterator(type, statnum));
}
static DThinkerIterator* CreateClientsideThinkerIterator(PClass* type, int statnum)
{
return Create<DThinkerIterator>(currentVMLevel, type, statnum, true);
}
DEFINE_ACTION_FUNCTION_NATIVE(DThinkerIterator, CreateClientside, CreateClientsideThinkerIterator)
{
PARAM_PROLOGUE;
PARAM_CLASS(type, DThinker);
PARAM_INT(statnum);
ACTION_RETURN_OBJECT(CreateClientsideThinkerIterator(type, statnum));
}
static DThinker *NextThinker(DThinkerIterator *self, bool exact)
{
return self->Next(exact);
@ -365,6 +378,19 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, CreateActorIterator, CreateActI)
ACTION_RETURN_OBJECT(CreateActI(self, tid, type));
}
static DActorIterator* CreateClientSideActI(FLevelLocals* Level, int tid, PClassActor* type)
{
return Create<DActorIterator>(Level->ClientSideTIDHash, type, tid);
}
DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, CreateClientSideActorIterator, CreateClientSideActI)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_INT(tid);
PARAM_CLASS(type, AActor);
ACTION_RETURN_OBJECT(CreateClientSideActI(self, tid, type));
}
static AActor *NextActI(DActorIterator *self)
{
return self->Next();

View file

@ -2615,6 +2615,26 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, setFrozen, setFrozen)
return 0;
}
static DThinker* CreateClientsideThinker(FLevelLocals* self, PClass* type, int statnum)
{
if (type->IsDescendantOf(NAME_Actor))
{
ThrowAbortException(X_OTHER, "Clientside Actors cannot be created from this function");
return nullptr;
}
return self->CreateClientsideThinker(type, statnum);
}
DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, CreateClientsideThinker, CreateClientsideThinker)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_POINTER_NOT_NULL(type, PClass);
PARAM_INT(statnum);
ACTION_RETURN_OBJECT(CreateClientsideThinker(self, type, statnum));
}
//=====================================================================================
//
//

View file

@ -999,9 +999,9 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetFloorTerrain, GetFloorTerrain)
ACTION_RETURN_POINTER(GetFloorTerrain(self));
}
static int P_FindUniqueTID(FLevelLocals *Level, int start, int limit)
static int P_FindUniqueTID(FLevelLocals *Level, int start, int limit, bool clientside)
{
return Level->FindUniqueTID(start, limit);
return Level->FindUniqueTID(start, limit, clientside);
}
DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, FindUniqueTid, P_FindUniqueTID)
@ -1009,7 +1009,8 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, FindUniqueTid, P_FindUniqueTID)
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_INT(start);
PARAM_INT(limit);
ACTION_RETURN_INT(P_FindUniqueTID(self, start, limit));
PARAM_BOOL(clientside);
ACTION_RETURN_INT(P_FindUniqueTID(self, start, limit, clientside));
}
static void RemoveFromHash(AActor *self)

View file

@ -786,6 +786,7 @@ class Actor : Thinker native
native bool CheckPosition(Vector2 pos, bool actorsonly = false, FCheckPosition tm = null);
native bool TestMobjLocation();
native static Actor Spawn(class<Actor> type, vector3 pos = (0,0,0), int replace = NO_REPLACE);
native static clearscope Actor SpawnClientside(class<Actor> type, vector3 pos = (0,0,0), int replace = NO_REPLACE);
native Actor SpawnMissile(Actor dest, class<Actor> type, Actor owner = null);
native Actor SpawnMissileXYZ(Vector3 pos, Actor dest, Class<Actor> type, bool checkspawn = true, Actor owner = null);
native Actor SpawnMissileZ (double z, Actor dest, class<Actor> type);

View file

@ -140,9 +140,6 @@ extend class Object
native static void MarkSound(Sound snd);
native static uint BAM(double angle);
native static void SetMusicVolume(float vol);
native clearscope static Object GetNetworkEntity(uint id);
native play void EnableNetworking(bool enable);
native clearscope uint GetNetworkID() const;
}
class Thinker : Object native play
@ -199,6 +196,7 @@ class Thinker : Object native play
class ThinkerIterator : Object native
{
native static ThinkerIterator Create(class<Object> type = "Actor", int statnum=Thinker.MAX_STATNUM+1);
native static ThinkerIterator CreateClientside(class<Thinker> type = "Actor", int statnum=Thinker.MAX_STATNUM+1);
native Thinker Next(bool exact = false);
native void Reinit();
}
@ -499,7 +497,7 @@ struct LevelLocals native
native bool IsFreelookAllowed() const;
native void StartIntermission(Name type, int state) const;
native play SpotState GetSpotState(bool create = true);
native int FindUniqueTid(int start = 0, int limit = 0);
native int FindUniqueTid(int start = 0, int limit = 0, bool clientside = false);
native uint GetSkyboxPortal(Actor actor);
native void ReplaceTextures(String from, String to, int flags);
clearscope native HealthGroup FindHealthGroup(int id);
@ -535,9 +533,11 @@ struct LevelLocals native
native void ChangeSky(TextureID sky1, TextureID sky2 );
native void ForceLightning(int mode = 0, sound tempSound = "");
native clearscope Thinker CreateClientsideThinker(class<Thinker> type, int statnum = Thinker.STAT_DEFAULT);
native SectorTagIterator CreateSectorTagIterator(int tag, line defline = null);
native LineIdIterator CreateLineIdIterator(int tag);
native ActorIterator CreateActorIterator(int tid, class<Actor> type = "Actor");
native ActorIterator CreateClientSideActorIterator(int tid, class<Actor> type = "Actor");
String TimeFormatted(bool totals = false)
{

View file

@ -779,6 +779,11 @@ class Object native
native static Function<void> FindFunction(Class<Object> cls, Name fn);
native clearscope static Object GetNetworkEntity(uint id);
native play void EnableNetworking(bool enable);
native clearscope uint GetNetworkID() const;
native clearscope bool IsClientside() const;
native virtualscope void Destroy();
// This does not call into the native method of the same name to avoid problems with objects that get garbage collected late on shutdown.