diff --git a/src/am_map.cpp b/src/am_map.cpp index ec6381754..627d6ea1e 100644 --- a/src/am_map.cpp +++ b/src/am_map.cpp @@ -261,7 +261,7 @@ CCMD(togglemap) { if (gameaction == ga_nothing) { - gameaction = ga_togglemap; + AM_ToggleMap(); } } diff --git a/src/common/objects/dobject.cpp b/src/common/objects/dobject.cpp index e124a975c..5b2658a70 100644 --- a/src/common/objects/dobject.cpp +++ b/src/common/objects/dobject.cpp @@ -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); diff --git a/src/common/objects/dobject.h b/src/common/objects/dobject.h index c0b6aecb5..9925cb88e 100644 --- a/src/common/objects/dobject.h +++ b/src/common/objects/dobject.h @@ -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(); diff --git a/src/common/objects/dobjgc.h b/src/common/objects/dobjgc.h index f5514255d..39f0011e0 100644 --- a/src/common/objects/dobjgc.h +++ b/src/common/objects/dobjgc.h @@ -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 TObjPtr; diff --git a/src/common/objects/dobjtype.cpp b/src/common/objects/dobjtype.cpp index eac10469a..a7c3dfb0f 100644 --- a/src/common/objects/dobjtype.cpp +++ b/src/common/objects/dobjtype.cpp @@ -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(this)); InitializeSpecials(mem, Defaults, &PClass::SpecialInits); diff --git a/src/d_net.cpp b/src/d_net.cpp index b14869a33..d3251fce1 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -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() diff --git a/src/g_game.cpp b/src/g_game.cpp index 88fc30144..30274dd93 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -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 = ""; } } diff --git a/src/g_level.cpp b/src/g_level.cpp index f39588cae..97a17a554 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -457,7 +457,7 @@ void G_NewInit () int i; // Destory all old player refrences that may still exist - TThinkerIterator it(primaryLevel, NAME_PlayerPawn, STAT_TRAVELLING); + TThinkerIterator 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) { diff --git a/src/g_levellocals.h b/src/g_levellocals.h index da3bc58c2..2bb55f332 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -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 TThinkerIterator GetThinkerIterator(FName subtype = NAME_None, int statnum = MAX_STATNUM+1) { - if (subtype == NAME_None) return TThinkerIterator(this, statnum); - else return TThinkerIterator(this, subtype, statnum); + if (subtype == NAME_None) return TThinkerIterator(this, statnum, false); + else return TThinkerIterator(this, subtype, statnum, false); } template TThinkerIterator GetThinkerIterator(FName subtype, int statnum, AActor *prev) { - return TThinkerIterator(this, subtype, statnum, prev); + return TThinkerIterator(this, subtype, statnum, prev, false); + } + template TThinkerIterator GetClientsideThinkerIterator(FName subtype = NAME_None, int statnum = MAX_STATNUM + 1) + { + if (subtype == NAME_None) return TThinkerIterator(this, statnum, true); + else return TThinkerIterator(this, subtype, statnum, true); + } + template TThinkerIterator GetClientsideThinkerIterator(FName subtype, int statnum, AActor* prev) + { + return TThinkerIterator(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)...); return thinker; } + + DThinker* CreateClientsideThinker(PClass* cls, int statnum = STAT_DEFAULT) + { + DThinker* thinker = static_cast(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 + T* CreateClientsideThinker(Args&&... args) + { + auto thinker = static_cast(CreateClientsideThinker(RUNTIME_CLASS(T), T::DEFAULT_STAT)); + thinker->Construct(std::forward(args)...); + return thinker; + } void SetMusic(); @@ -511,6 +552,7 @@ public: FBehaviorContainer Behaviors; AActor *TIDHash[128]; + AActor* ClientSideTIDHash[128]; TArray StrifeDialogues; FDialogueIDMap DialogueRoots; @@ -674,6 +716,7 @@ public: TArray Particles; TArray ParticlesInSubsec; FThinkerCollection Thinkers; + FThinkerCollection ClientsideThinkers; TArray Scrolls; // NULL if no DScrollers in this level @@ -710,6 +753,7 @@ public: TArray> CorpseQueue; TObjPtr FraggleScriptThinker = MakeObjPtr(nullptr); TObjPtr ACSThinker = MakeObjPtr(nullptr); + TObjPtr ClientSideACSThinker = MakeObjPtr(nullptr); TObjPtr SpotState = MakeObjPtr(nullptr); diff --git a/src/gamedata/info.cpp b/src/gamedata/info.cpp index 3fc5d2e76..1cd624e22 100644 --- a/src/gamedata/info.cpp +++ b/src/gamedata/info.cpp @@ -63,6 +63,7 @@ extern void ClearStrifeTypes(); TArray 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 { diff --git a/src/gamedata/info.h b/src/gamedata/info.h index 69ad560e0..8d2f20c19 100644 --- a/src/gamedata/info.h +++ b/src/gamedata/info.h @@ -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; }; diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index d759add72..659e67664 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -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. diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 85b592bb3..c18cb1621 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -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 diff --git a/src/p_tick.cpp b/src/p_tick.cpp index efe35f412..1837fb3da 100644 --- a/src/p_tick.cpp +++ b/src/p_tick.cpp @@ -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* 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. } diff --git a/src/playsim/dthinker.cpp b/src/playsim/dthinker.cpp index 51cf3a670..05e5e56c3 100644 --- a/src/playsim/dthinker.cpp +++ b/src/playsim/dthinker.cpp @@ -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(); + + // 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; diff --git a/src/playsim/dthinker.h b/src/playsim/dthinker.h index 78dcd1520..ab4a453b8 100644 --- a/src/playsim/dthinker.h +++ b/src/playsim/dthinker.h @@ -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 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) diff --git a/src/playsim/p_acs.cpp b/src/playsim/p_acs.cpp index 34b78219a..6fac4c79c 100644 --- a/src/playsim/p_acs.cpp +++ b/src/playsim/p_acs.cpp @@ -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 controller; int script; TArray 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 (l, who, where, num, code, module, args, argcount, flags); + return Create (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(); + if (clientside) + { + if (Level->ClientSideACSThinker == nullptr) + Level->ClientSideACSThinker = Level->CreateClientsideThinker(); + + controller = Level->ClientSideACSThinker; + } + else + { + if (Level->ACSThinker == nullptr) + Level->ACSThinker = Level->CreateThinker(); + + 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) diff --git a/src/playsim/p_acs.h b/src/playsim/p_acs.h index 695b22948..0ac9f139b 100644 --- a/src/playsim/p_acs.h +++ b/src/playsim/p_acs.h @@ -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 }; diff --git a/src/playsim/p_maputl.cpp b/src/playsim/p_maputl.cpp index 8c172c479..7379ff743 100644 --- a/src/playsim/p_maputl.cpp +++ b/src/playsim/p_maputl.cpp @@ -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, §or_t::touching_thinglist); - P_DelSeclist(touching_rendersectors, §or_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, §or_t::touching_thinglist); + P_DelSeclist(touching_rendersectors, §or_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 = §or->thinglist; - AActor *next = *link; - if ((snext = next)) - next->sprev = &snext; - sprev = link; - *link = this; + AActor** link = §or->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, §or_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, §or_t::touching_thinglist); // Attach to thing if (renderradius >= 0) touching_rendersectors = P_CreateSecNodeList(this, RenderRadius(), ctx != nullptr ? ctx->render_list : nullptr, §or_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; diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 216d13db7..8dc686f71 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -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(Level->CreateThinker(type)); - actor->EnableNetworking(true); + if (GetDefaultByType(type)->ObjectFlags & OF_ClientSide) + { + actor = static_cast(Level->CreateClientsideThinker(type)); + } + else + { + actor = static_cast(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); diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 61fd17cee..956337331 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -157,15 +157,6 @@ static TArray PredictionSectorListBackup; static TArray PredictionTouchingSectorsBackup; static TArray PredictionTouchingSectors_sprev_Backup; -static TArray PredictionRenderSectorsBackup; -static TArray PredictionRenderSectors_sprev_Backup; - -static TArray PredictionPortalSectorsBackup; -static TArray PredictionPortalSectors_sprev_Backup; - -static TArray PredictionPortalLinesBackup; -static TArray PredictionPortalLines_sprev_Backup; - struct { DVector3 Pos = {}; @@ -1381,19 +1372,9 @@ void BackupNodeList(AActor *act, nodetype *head, nodetype *linktype::*otherlist, } template -nodetype *RestoreNodeList(AActor *act, nodetype *head, nodetype *linktype::*otherlist, TArray &prevbackup, TArray &otherbackup) +nodetype *RestoreNodeList(AActor *act, nodetype *linktype::*otherlist, TArray &prevbackup, TArray &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, §or_t::touching_thinglist, PredictionTouchingSectors_sprev_Backup, PredictionTouchingSectorsBackup); - BackupNodeList(act, act->touching_rendersectors, §or_t::touching_renderthings, PredictionRenderSectors_sprev_Backup, PredictionRenderSectorsBackup); - BackupNodeList(act, act->touching_sectorportallist, §or_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, §or_t::touching_thinglist, PredictionTouchingSectors_sprev_Backup, PredictionTouchingSectorsBackup); - act->touching_rendersectors = RestoreNodeList(act, ctx.render_list, §or_t::touching_renderthings, PredictionRenderSectors_sprev_Backup, PredictionRenderSectorsBackup); - act->touching_sectorportallist = RestoreNodeList(act, sectorportal_list, §or_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, §or_t::touching_thinglist, PredictionTouchingSectors_sprev_Backup, PredictionTouchingSectorsBackup); + if (act->renderradius >= 0.0) + act->touching_rendersectors = P_CreateSecNodeList(act, act->RenderRadius(), nullptr, §or_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; } diff --git a/src/scripting/thingdef_data.cpp b/src/scripting/thingdef_data.cpp index e17e28c35..7c301c835 100644 --- a/src/scripting/thingdef_data.cpp +++ b/src/scripting/thingdef_data.cpp @@ -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), }; diff --git a/src/scripting/vmiterators.cpp b/src/scripting/vmiterators.cpp index ec053e5fe..730f09226 100644 --- a/src/scripting/vmiterators.cpp +++ b/src/scripting/vmiterators.cpp @@ -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(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(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(); diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index a43c2f468..6ff757b96 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -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)); +} + //===================================================================================== // // diff --git a/src/scripting/vmthunks_actors.cpp b/src/scripting/vmthunks_actors.cpp index 018025577..f5efbab3a 100644 --- a/src/scripting/vmthunks_actors.cpp +++ b/src/scripting/vmthunks_actors.cpp @@ -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) diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index c27dbf8ba..31b44fa1d 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -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 type, vector3 pos = (0,0,0), int replace = NO_REPLACE); + native static clearscope Actor SpawnClientside(class type, vector3 pos = (0,0,0), int replace = NO_REPLACE); native Actor SpawnMissile(Actor dest, class type, Actor owner = null); native Actor SpawnMissileXYZ(Vector3 pos, Actor dest, Class type, bool checkspawn = true, Actor owner = null); native Actor SpawnMissileZ (double z, Actor dest, class type); diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index c4582c15e..9a3e9fe04 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -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 type = "Actor", int statnum=Thinker.MAX_STATNUM+1); + native static ThinkerIterator CreateClientside(class 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 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 type = "Actor"); + native ActorIterator CreateClientSideActorIterator(int tid, class type = "Actor"); String TimeFormatted(bool totals = false) { diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index 33c14eef6..04789cdd4 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -779,6 +779,11 @@ class Object native native static Function FindFunction(Class 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.